typir
Version:
General purpose type checking library
276 lines • 13 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 { WaitingForIdentifiableAndCompletedTypeReferences, WaitingForInvalidTypeReferences } from '../initialization/type-waiting.js';
import { assertTrue, assertUnreachable, removeFromArray } from '../utils/utils.js';
/**
* Design decisions:
* - features of types are realized/determined by their kinds
* - Identifiers of types must be unique!
*/
export class Type {
constructor(identifier, typeDetails) {
// this is required only to apply graph algorithms in a generic way!
// $relation is used as key
this.edgesIncoming = new Map();
this.edgesOutgoing = new Map();
// store the state of the initialization process of this type
this.initializationState = 'Invalid';
// manage listeners for updates of the initialization state
this.stateListeners = [];
this.identifier = identifier;
this.associatedLanguageNode = typeDetails.associatedLanguageNode;
}
/**
* Identifiers must be unique and stable for all types known in a single Typir instance, since they are used as key to store types in maps.
* Identifiers might have a naming schema for calculatable values.
*/
getIdentifier() {
// an Identifier must be available; note that the state might be 'Invalid' nevertheless, which is required to handle cyclic type definitions
assertTrue(this.identifier !== undefined);
return this.identifier;
}
getInitializationState() {
return this.initializationState;
}
assertState(expectedState) {
if (this.isInState(expectedState) === false) {
throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but ${expectedState} is expected.`);
}
}
assertNotState(expectedState) {
if (this.isNotInState(expectedState) === false) {
throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but this state is not expected.`);
}
}
assertStateOrLater(expectedState) {
if (this.isInStateOrLater(expectedState) === false) {
throw new Error(`The current state of type '${this.identifier}' is ${this.initializationState}, but this state is not expected.`);
}
}
isInState(state) {
return this.initializationState === state;
}
isNotInState(state) {
return this.initializationState !== state;
}
isInStateOrLater(state) {
switch (state) {
case 'Invalid':
return true;
case 'Identifiable':
return this.initializationState !== 'Invalid';
case 'Completed':
return this.initializationState === 'Completed';
default:
assertUnreachable(state);
}
}
addListener(newListeners, informIfNotInvalidAnymore) {
this.stateListeners.push(newListeners);
if (informIfNotInvalidAnymore) {
const currentState = this.getInitializationState();
switch (currentState) {
case 'Invalid':
// don't inform about the Invalid state!
break;
case 'Identifiable':
newListeners.onSwitchedToIdentifiable(this);
break;
case 'Completed':
newListeners.onSwitchedToIdentifiable(this); // inform about both Identifiable and Completed!
newListeners.onSwitchedToCompleted(this);
break;
default:
assertUnreachable(currentState);
}
}
}
removeListener(listener) {
removeFromArray(listener, this.stateListeners);
}
/**
* Use this method to specify, how THIS new type should be initialized.
*
* This method has(!) to be called at the end(!) of the constructor of each specific Type implementation, even if nothing has to be specified,
* since calling this method starts the initialization process!
* If you forget the call this method, the new type remains invalid and invisible for Typir and you will not be informed about this problem!
*
* @param preconditions all possible options for the initialization process
*/
defineTheInitializationProcessOfThisType(preconditions) {
var _a, _b, _c, _d, _e, _f, _g, _h;
// store the reactions
this.onIdentification = (_a = preconditions.onIdentifiable) !== null && _a !== void 0 ? _a : (() => { });
this.onCompletion = (_b = preconditions.onCompleted) !== null && _b !== void 0 ? _b : (() => { });
this.onInvalidation = (_c = preconditions.onInvalidated) !== null && _c !== void 0 ? _c : (() => { });
// preconditions for Identifiable
this.waitForIdentifiable = new WaitingForIdentifiableAndCompletedTypeReferences((_d = preconditions.preconditionsForIdentifiable) === null || _d === void 0 ? void 0 : _d.referencesToBeIdentifiable, (_e = preconditions.preconditionsForIdentifiable) === null || _e === void 0 ? void 0 : _e.referencesToBeCompleted);
this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this])); // start of the principle: children don't need to wait for their parents
// preconditions for Completed
this.waitForCompleted = new WaitingForIdentifiableAndCompletedTypeReferences((_f = preconditions.preconditionsForCompleted) === null || _f === void 0 ? void 0 : _f.referencesToBeIdentifiable, (_g = preconditions.preconditionsForCompleted) === null || _g === void 0 ? void 0 : _g.referencesToBeCompleted);
this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this])); // start of the principle: children don't need to wait for their parents
// preconditions for Invalid
this.waitForInvalid = new WaitingForInvalidTypeReferences((_h = preconditions.referencesRelevantForInvalidation) !== null && _h !== void 0 ? _h : []);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisType = this;
// invalid --> identifiable
this.waitForIdentifiable.addListener({
onFulfilled(_waiter) {
thisType.switchFromInvalidToIdentifiable();
if (thisType.waitForCompleted.isFulfilled()) {
// this is required to ensure the stric order Identifiable --> Completed, since 'waitForCompleted' might already be triggered
thisType.switchFromIdentifiableToCompleted();
}
},
onInvalidated(_waiter) {
thisType.switchFromCompleteOrIdentifiableToInvalid();
},
}, true); // 'true' triggers the initialization process!
// identifiable --> completed
this.waitForCompleted.addListener({
onFulfilled(_waiter) {
if (thisType.waitForIdentifiable.isFulfilled()) {
thisType.switchFromIdentifiableToCompleted();
}
else {
// switching will be done later by 'waitForIdentifiable' in order to conform to the stric order Identifiable --> Completed
}
},
onInvalidated(_waiter) {
thisType.switchFromCompleteOrIdentifiableToInvalid();
},
}, false); // not required, since 'waitForIdentifiable' will switch to Completed as well!
// identifiable/completed --> invalid
this.waitForInvalid.addListener(() => {
this.switchFromCompleteOrIdentifiableToInvalid();
}, false); // no initial trigger, since 'Invalid' is the initial state
}
/**
* This is an internal method to ignore some types during the initialization process in order to prevent dependency cycles.
* Usually there is no need to call this method on your own.
* @param additionalTypesToIgnore the new types to ignore during
*/
ignoreDependingTypesDuringInitialization(additionalTypesToIgnore) {
this.waitForIdentifiable.addTypesToIgnoreForCycles(additionalTypesToIgnore);
this.waitForCompleted.addTypesToIgnoreForCycles(additionalTypesToIgnore);
}
dispose() {
// clear everything
this.stateListeners.splice(0, this.stateListeners.length);
this.waitForInvalid.getWaitForRefsInvalid().forEach(ref => ref.dispose());
this.waitForIdentifiable.deconstruct();
this.waitForCompleted.deconstruct();
this.waitForInvalid.deconstruct();
// edges are already removed, when the type is removed from the graph,
// but in some cases, the type was not (yet) added to the graph, but got already edges => these edges need to be removed now
// e.g. "duplicated" types which are created and disposed by TypeInitializers
this.edgesIncoming.clear();
this.edgesOutgoing.clear();
}
switchFromInvalidToIdentifiable() {
this.assertState('Invalid');
this.onIdentification();
this.initializationState = 'Identifiable';
this.stateListeners.slice().forEach(listener => listener.onSwitchedToIdentifiable(this)); // slice() prevents issues with removal of listeners during notifications
}
switchFromIdentifiableToCompleted() {
this.assertState('Identifiable');
this.onCompletion();
this.initializationState = 'Completed';
this.stateListeners.slice().forEach(listener => listener.onSwitchedToCompleted(this)); // slice() prevents issues with removal of listeners during notifications
}
switchFromCompleteOrIdentifiableToInvalid() {
if (this.isNotInState('Invalid')) {
this.onInvalidation();
this.initializationState = 'Invalid';
this.stateListeners.slice().forEach(listener => listener.onSwitchedToInvalid(this)); // slice() prevents issues with removal of listeners during notifications
// add the types again, since the initialization process started again
this.waitForIdentifiable.addTypesToIgnoreForCycles(new Set([this]));
this.waitForCompleted.addTypesToIgnoreForCycles(new Set([this]));
}
else {
// is already 'Invalid' => do nothing
}
}
addIncomingEdge(edge) {
const key = edge.$relation;
if (this.edgesIncoming.has(key)) {
this.edgesIncoming.get(key).push(edge);
}
else {
this.edgesIncoming.set(key, [edge]);
}
}
addOutgoingEdge(edge) {
const key = edge.$relation;
if (this.edgesOutgoing.has(key)) {
this.edgesOutgoing.get(key).push(edge);
}
else {
this.edgesOutgoing.set(key, [edge]);
}
}
removeIncomingEdge(edge) {
const key = edge.$relation;
const list = this.edgesIncoming.get(key);
if (list) {
const index = list.indexOf(edge);
if (index >= 0) {
list.splice(index, 1);
if (list.length <= 0) {
this.edgesIncoming.delete(key);
}
return true;
}
}
return false;
}
removeOutgoingEdge(edge) {
const key = edge.$relation;
const list = this.edgesOutgoing.get(key);
if (list) {
const index = list.indexOf(edge);
if (index >= 0) {
list.splice(index, 1);
if (list.length <= 0) {
this.edgesOutgoing.delete(key);
}
return true;
}
}
return false;
}
getIncomingEdges($relation) {
var _a;
return (_a = this.edgesIncoming.get($relation)) !== null && _a !== void 0 ? _a : [];
}
getOutgoingEdges($relation) {
var _a;
return (_a = this.edgesOutgoing.get($relation)) !== null && _a !== void 0 ? _a : [];
}
getEdges($relation) {
return [
...this.getIncomingEdges($relation),
...this.getOutgoingEdges($relation),
];
}
getAllIncomingEdges() {
return Array.from(this.edgesIncoming.values()).flat();
}
getAllOutgoingEdges() {
return Array.from(this.edgesOutgoing.values()).flat();
}
getAllEdges() {
return [
...this.getAllIncomingEdges(),
...this.getAllOutgoingEdges(),
];
}
}
export function isType(type) {
return typeof type === 'object' && type !== null && typeof type.getIdentifier === 'function' && typeof type.kind === 'object';
}
//# sourceMappingURL=type-node.js.map