282 lines
11 KiB
JavaScript
282 lines
11 KiB
JavaScript
|
import { Deferred } from '@firebase/util';
|
||
|
|
||
|
/**
|
||
|
* Component for service name T, e.g. `auth`, `auth-internal`
|
||
|
*/
|
||
|
class Component {
|
||
|
/**
|
||
|
*
|
||
|
* @param name The public service name, e.g. app, auth, firestore, database
|
||
|
* @param instanceFactory Service factory responsible for creating the public interface
|
||
|
* @param type whether the service provided by the component is public or private
|
||
|
*/
|
||
|
constructor(name, instanceFactory, type) {
|
||
|
this.name = name;
|
||
|
this.instanceFactory = instanceFactory;
|
||
|
this.type = type;
|
||
|
this.multipleInstances = false;
|
||
|
/**
|
||
|
* Properties to be added to the service namespace
|
||
|
*/
|
||
|
this.serviceProps = {};
|
||
|
this.instantiationMode = "LAZY" /* LAZY */;
|
||
|
}
|
||
|
setInstantiationMode(mode) {
|
||
|
this.instantiationMode = mode;
|
||
|
return this;
|
||
|
}
|
||
|
setMultipleInstances(multipleInstances) {
|
||
|
this.multipleInstances = multipleInstances;
|
||
|
return this;
|
||
|
}
|
||
|
setServiceProps(props) {
|
||
|
this.serviceProps = props;
|
||
|
return this;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @license
|
||
|
* Copyright 2019 Google LLC
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
const DEFAULT_ENTRY_NAME = '[DEFAULT]';
|
||
|
|
||
|
/**
|
||
|
* @license
|
||
|
* Copyright 2019 Google LLC
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
/**
|
||
|
* Provider for instance for service name T, e.g. 'auth', 'auth-internal'
|
||
|
* NameServiceMapping[T] is an alias for the type of the instance
|
||
|
*/
|
||
|
class Provider {
|
||
|
constructor(name, container) {
|
||
|
this.name = name;
|
||
|
this.container = container;
|
||
|
this.component = null;
|
||
|
this.instances = new Map();
|
||
|
this.instancesDeferred = new Map();
|
||
|
}
|
||
|
/**
|
||
|
* @param identifier A provider can provide mulitple instances of a service
|
||
|
* if this.component.multipleInstances is true.
|
||
|
*/
|
||
|
get(identifier = DEFAULT_ENTRY_NAME) {
|
||
|
// if multipleInstances is not supported, use the default name
|
||
|
const normalizedIdentifier = this.normalizeInstanceIdentifier(identifier);
|
||
|
if (!this.instancesDeferred.has(normalizedIdentifier)) {
|
||
|
const deferred = new Deferred();
|
||
|
this.instancesDeferred.set(normalizedIdentifier, deferred);
|
||
|
// If the service instance is available, resolve the promise with it immediately
|
||
|
try {
|
||
|
const instance = this.getOrInitializeService(normalizedIdentifier);
|
||
|
if (instance) {
|
||
|
deferred.resolve(instance);
|
||
|
}
|
||
|
}
|
||
|
catch (e) {
|
||
|
// when the instance factory throws an exception during get(), it should not cause
|
||
|
// a fatal error. We just return the unresolved promise in this case.
|
||
|
}
|
||
|
}
|
||
|
return this.instancesDeferred.get(normalizedIdentifier).promise;
|
||
|
}
|
||
|
getImmediate(options) {
|
||
|
const { identifier, optional } = Object.assign({ identifier: DEFAULT_ENTRY_NAME, optional: false }, options);
|
||
|
// if multipleInstances is not supported, use the default name
|
||
|
const normalizedIdentifier = this.normalizeInstanceIdentifier(identifier);
|
||
|
try {
|
||
|
const instance = this.getOrInitializeService(normalizedIdentifier);
|
||
|
if (!instance) {
|
||
|
if (optional) {
|
||
|
return null;
|
||
|
}
|
||
|
throw Error(`Service ${this.name} is not available`);
|
||
|
}
|
||
|
return instance;
|
||
|
}
|
||
|
catch (e) {
|
||
|
if (optional) {
|
||
|
return null;
|
||
|
}
|
||
|
else {
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
getComponent() {
|
||
|
return this.component;
|
||
|
}
|
||
|
setComponent(component) {
|
||
|
if (component.name !== this.name) {
|
||
|
throw Error(`Mismatching Component ${component.name} for Provider ${this.name}.`);
|
||
|
}
|
||
|
if (this.component) {
|
||
|
throw Error(`Component for ${this.name} has already been provided`);
|
||
|
}
|
||
|
this.component = component;
|
||
|
// if the service is eager, initialize the default instance
|
||
|
if (isComponentEager(component)) {
|
||
|
try {
|
||
|
this.getOrInitializeService(DEFAULT_ENTRY_NAME);
|
||
|
}
|
||
|
catch (e) {
|
||
|
// when the instance factory for an eager Component throws an exception during the eager
|
||
|
// initialization, it should not cause a fatal error.
|
||
|
// TODO: Investigate if we need to make it configurable, because some component may want to cause
|
||
|
// a fatal error in this case?
|
||
|
}
|
||
|
}
|
||
|
// Create service instances for the pending promises and resolve them
|
||
|
// NOTE: if this.multipleInstances is false, only the default instance will be created
|
||
|
// and all promises with resolve with it regardless of the identifier.
|
||
|
for (const [instanceIdentifier, instanceDeferred] of this.instancesDeferred.entries()) {
|
||
|
const normalizedIdentifier = this.normalizeInstanceIdentifier(instanceIdentifier);
|
||
|
try {
|
||
|
// `getOrInitializeService()` should always return a valid instance since a component is guaranteed. use ! to make typescript happy.
|
||
|
const instance = this.getOrInitializeService(normalizedIdentifier);
|
||
|
instanceDeferred.resolve(instance);
|
||
|
}
|
||
|
catch (e) {
|
||
|
// when the instance factory throws an exception, it should not cause
|
||
|
// a fatal error. We just leave the promise unresolved.
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
clearInstance(identifier = DEFAULT_ENTRY_NAME) {
|
||
|
this.instancesDeferred.delete(identifier);
|
||
|
this.instances.delete(identifier);
|
||
|
}
|
||
|
// app.delete() will call this method on every provider to delete the services
|
||
|
// TODO: should we mark the provider as deleted?
|
||
|
async delete() {
|
||
|
const services = Array.from(this.instances.values());
|
||
|
await Promise.all(services
|
||
|
.filter(service => 'INTERNAL' in service)
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
.map(service => service.INTERNAL.delete()));
|
||
|
}
|
||
|
isComponentSet() {
|
||
|
return this.component != null;
|
||
|
}
|
||
|
getOrInitializeService(identifier) {
|
||
|
let instance = this.instances.get(identifier);
|
||
|
if (!instance && this.component) {
|
||
|
instance = this.component.instanceFactory(this.container, normalizeIdentifierForFactory(identifier));
|
||
|
this.instances.set(identifier, instance);
|
||
|
}
|
||
|
return instance || null;
|
||
|
}
|
||
|
normalizeInstanceIdentifier(identifier) {
|
||
|
if (this.component) {
|
||
|
return this.component.multipleInstances ? identifier : DEFAULT_ENTRY_NAME;
|
||
|
}
|
||
|
else {
|
||
|
return identifier; // assume multiple instances are supported before the component is provided.
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// undefined should be passed to the service factory for the default instance
|
||
|
function normalizeIdentifierForFactory(identifier) {
|
||
|
return identifier === DEFAULT_ENTRY_NAME ? undefined : identifier;
|
||
|
}
|
||
|
function isComponentEager(component) {
|
||
|
return component.instantiationMode === "EAGER" /* EAGER */;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @license
|
||
|
* Copyright 2019 Google LLC
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
/**
|
||
|
* ComponentContainer that provides Providers for service name T, e.g. `auth`, `auth-internal`
|
||
|
*/
|
||
|
class ComponentContainer {
|
||
|
constructor(name) {
|
||
|
this.name = name;
|
||
|
this.providers = new Map();
|
||
|
}
|
||
|
/**
|
||
|
*
|
||
|
* @param component Component being added
|
||
|
* @param overwrite When a component with the same name has already been registered,
|
||
|
* if overwrite is true: overwrite the existing component with the new component and create a new
|
||
|
* provider with the new component. It can be useful in tests where you want to use different mocks
|
||
|
* for different tests.
|
||
|
* if overwrite is false: throw an exception
|
||
|
*/
|
||
|
addComponent(component) {
|
||
|
const provider = this.getProvider(component.name);
|
||
|
if (provider.isComponentSet()) {
|
||
|
throw new Error(`Component ${component.name} has already been registered with ${this.name}`);
|
||
|
}
|
||
|
provider.setComponent(component);
|
||
|
}
|
||
|
addOrOverwriteComponent(component) {
|
||
|
const provider = this.getProvider(component.name);
|
||
|
if (provider.isComponentSet()) {
|
||
|
// delete the existing provider from the container, so we can register the new component
|
||
|
this.providers.delete(component.name);
|
||
|
}
|
||
|
this.addComponent(component);
|
||
|
}
|
||
|
/**
|
||
|
* getProvider provides a type safe interface where it can only be called with a field name
|
||
|
* present in NameServiceMapping interface.
|
||
|
*
|
||
|
* Firebase SDKs providing services should extend NameServiceMapping interface to register
|
||
|
* themselves.
|
||
|
*/
|
||
|
getProvider(name) {
|
||
|
if (this.providers.has(name)) {
|
||
|
return this.providers.get(name);
|
||
|
}
|
||
|
// create a Provider for a service that hasn't registered with Firebase
|
||
|
const provider = new Provider(name, this);
|
||
|
this.providers.set(name, provider);
|
||
|
return provider;
|
||
|
}
|
||
|
getProviders() {
|
||
|
return Array.from(this.providers.values());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export { Component, ComponentContainer, Provider };
|
||
|
//# sourceMappingURL=index.esm2017.js.map
|