typir
Version:
General purpose type checking library
215 lines • 11.1 kB
JavaScript
/******************************************************************************
* Copyright 2024 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import { removeFromArray, toArray } from '../utils/utils.js';
/**
* The purpose of this class is to inform its listeners, when all given TypeReferences reached their specified initialization state (or a later state).
* After that, the listeners might be informed multiple times,
* if at least one of the TypeReferences was unresolved/invalid, but later on all TypeReferences are again in the desired state, and so on.
*/
export class WaitingForIdentifiableAndCompletedTypeReferences {
constructor(waitForRefsToBeIdentified, waitForRefsToBeCompleted) {
/** Remembers whether all TypeReferences are in the desired states (true) or not (false). */
this.fulfilled = false;
/** This is required for cyclic type definitions:
* In case of two types A, B which use each other for their properties (e.g. class A {p: B} and class B {p: A}), the case easily occurs,
* that the types A and B (respectively their WaitingFor... instances) are waiting for each other and therefore waiting for each other.
* In order to solve these cycles, types which are part of such "dependency cycles" should be ignored during waiting,
* e.g. A should not waiting B and B should not wait for A.
* These types to ignore are stored in the following Set.
*/
this.typesToIgnoreForCycles = new Set();
/** These listeners will be informed once, when all TypeReferences are in the desired state.
* If some of these TypeReferences are invalid (the listeners will not be informed about this) and later in the desired state again,
* the listeners will be informed again, and so on. */
this.listeners = [];
// remember the relevant TypeReferences to wait for
this.waitForRefsIdentified = waitForRefsToBeIdentified;
this.waitForRefsCompleted = waitForRefsToBeCompleted;
// register to get updates for the relevant TypeReferences
toArray(this.waitForRefsIdentified).forEach(ref => ref.addListener(this, true)); // 'true' calls 'checkIfFulfilled()' to check, whether everything might already be fulfilled
toArray(this.waitForRefsCompleted).forEach(ref => ref.addListener(this, true));
}
deconstruct() {
var _a, _b;
this.listeners.splice(0, this.listeners.length);
(_a = this.waitForRefsIdentified) === null || _a === void 0 ? void 0 : _a.forEach(ref => ref.removeListener(this));
(_b = this.waitForRefsCompleted) === null || _b === void 0 ? void 0 : _b.forEach(ref => ref.removeListener(this));
this.typesToIgnoreForCycles.clear();
}
addListener(newListener, informAboutCurrentState) {
this.listeners.push(newListener);
// inform the new listener
if (informAboutCurrentState) {
if (this.fulfilled) {
newListener.onFulfilled(this);
}
else {
newListener.onInvalidated(this);
}
}
}
removeListener(listenerToRemove) {
removeFromArray(listenerToRemove, this.listeners);
}
/**
* This method is called to inform about additional types which can be ignored during the waiting/resolving process.
* This helps to deal with cycles in type dependencies.
* @param moreTypesToIgnore might contain duplicates, which are filtered internally
*/
addTypesToIgnoreForCycles(moreTypesToIgnore) {
var _a, _b;
// identify the actual new types to ignore (filtering out the types which are already ignored)
const newTypesToIgnore = new Set();
for (const typeToIgnore of moreTypesToIgnore) {
if (this.typesToIgnoreForCycles.has(typeToIgnore)) {
// ignore this additional type, required to break the propagation, since the propagation itself becomes cyclic as well in case of cyclic types!
}
else {
newTypesToIgnore.add(typeToIgnore);
this.typesToIgnoreForCycles.add(typeToIgnore);
}
}
if (newTypesToIgnore.size <= 0) {
// no new types to ignore => do nothing
}
else {
// propagate the new types to ignore recursively to all direct and indirect referenced types ...
// ... which should be identifiable (or completed)
for (const ref of ((_a = this.waitForRefsIdentified) !== null && _a !== void 0 ? _a : [])) {
const refType = ref.getType();
if (refType === null || refType === void 0 ? void 0 : refType.isInStateOrLater('Identifiable')) {
// this reference is already ready
}
else {
refType === null || refType === void 0 ? void 0 : refType.ignoreDependingTypesDuringInitialization(newTypesToIgnore);
}
}
// ... which should be completed
for (const ref of ((_b = this.waitForRefsCompleted) !== null && _b !== void 0 ? _b : [])) {
const refType = ref.getType();
if (refType === null || refType === void 0 ? void 0 : refType.isInStateOrLater('Completed')) {
// this reference is already ready
}
else {
refType === null || refType === void 0 ? void 0 : refType.ignoreDependingTypesDuringInitialization(newTypesToIgnore);
}
}
// since there are more types to ignore, check again
this.checkIfFulfilled();
}
}
onTypeReferenceResolved(_reference, resolvedType) {
// inform the referenced type about the types to ignore for completion, so that the type could switch to its next phase (if needed)
resolvedType.ignoreDependingTypesDuringInitialization(this.typesToIgnoreForCycles);
resolvedType.addListener(this, false);
// check, whether all TypeReferences are resolved and the resolved types are in the expected state
this.checkIfFulfilled();
// TODO is a more performant solution possible, e.g. by counting or using "resolvedType"?
}
onTypeReferenceInvalidated(_reference, previousType) {
// since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again)
this.switchToNotFulfilled();
if (previousType) {
previousType.removeListener(this);
}
}
onSwitchedToIdentifiable(_type) {
// check, whether all TypeReferences are resolved and the resolved types are in the expected state
this.checkIfFulfilled();
// TODO is a more performant solution possible, e.g. by counting or using "resolvedType"?
}
onSwitchedToCompleted(_type) {
// check, whether all TypeReferences are resolved and the resolved types are in the expected state
this.checkIfFulfilled();
// TODO is a more performant solution possible, e.g. by counting or using "resolvedType"?
}
onSwitchedToInvalid(_type) {
// since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again)
this.switchToNotFulfilled();
}
checkIfFulfilled() {
// already informed => do not inform again
if (this.fulfilled) {
return;
}
for (const ref of toArray(this.waitForRefsIdentified)) {
const refType = ref.getType();
if (refType && (refType.isInStateOrLater('Identifiable') || this.typesToIgnoreForCycles.has(refType))) {
// that is fine
}
else {
return;
}
}
for (const ref of toArray(this.waitForRefsCompleted)) {
const refType = ref.getType();
if (refType && (refType.isInStateOrLater('Completed') || this.typesToIgnoreForCycles.has(refType))) {
// that is fine
}
else {
return;
}
}
// everything is fine now! => inform all listeners
this.fulfilled = true; // don't inform the listeners again
this.listeners.slice().forEach(listener => listener.onFulfilled(this)); // slice() prevents issues with removal of listeners during notifications
this.typesToIgnoreForCycles.clear(); // otherwise deleted types remain in this Set forever
}
switchToNotFulfilled() {
// since at least one TypeReference was reset, the listeners might be informed (again), when all TypeReferences reached the desired state (again)
if (this.fulfilled) {
this.fulfilled = false;
this.listeners.slice().forEach(listener => listener.onInvalidated(this)); // slice() prevents issues with removal of listeners during notifications
}
else {
// already not fulfilled => nothing to do now
}
}
isFulfilled() {
return this.fulfilled;
}
}
export class WaitingForInvalidTypeReferences {
constructor(waitForRefsToBeInvalid) {
/** These listeners will be informed, when all TypeReferences are in the desired state. */
this.listeners = [];
// remember the relevant TypeReferences
this.waitForRefsInvalid = waitForRefsToBeInvalid;
this.counterInvalid = this.waitForRefsInvalid.filter(ref => ref.getType() === undefined || ref.getType().isInState('Invalid')).length;
// register to get updates for the relevant TypeReferences
this.waitForRefsInvalid.forEach(ref => ref.addListener(this, false));
}
deconstruct() {
this.listeners.splice(0, this.listeners.length);
this.waitForRefsInvalid.forEach(ref => ref.removeListener(this));
}
addListener(newListener, informIfAlreadyFulfilled) {
this.listeners.push(newListener);
// inform new listener, if the state is already reached!
if (informIfAlreadyFulfilled && this.isFulfilled()) {
newListener(this);
}
}
removeListener(listenerToRemove) {
removeFromArray(listenerToRemove, this.listeners);
}
onTypeReferenceResolved(_reference, _resolvedType) {
this.counterInvalid--;
}
onTypeReferenceInvalidated(_reference, _previousType) {
this.counterInvalid++;
if (this.isFulfilled()) {
this.listeners.slice().forEach(listener => listener(this));
}
}
isFulfilled() {
return this.counterInvalid === this.waitForRefsInvalid.length && this.waitForRefsInvalid.length >= 1;
}
getWaitForRefsInvalid() {
return this.waitForRefsInvalid;
}
}
//# sourceMappingURL=type-waiting.js.map