Merge pull request #5426 from ad1992/aakansha/typesafe

fix: make sequenceDB typesafe
This commit is contained in:
Sidharth Vinod
2024-04-14 04:09:05 +00:00
committed by GitHub
4 changed files with 187 additions and 79 deletions

View File

@@ -53,6 +53,7 @@ module.exports = {
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/consistent-type-definitions': 'warn',
'@typescript-eslint/ban-ts-comment': [
'error',
{

View File

@@ -1,5 +1,6 @@
import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js';
import { ImperativeState } from '../../utils/imperativeState.js';
import { sanitizeText } from '../common/common.js';
import {
clear as commonClear,
@@ -10,9 +11,24 @@ import {
setAccTitle,
setDiagramTitle,
} from '../common/commonDb.js';
import { ImperativeState } from '../../utils/imperativeState.js';
import type { Actor, AddMessageParams, Box, Message, Note } from './types.js';
const state = new ImperativeState(() => ({
interface SequenceState {
prevActor?: string;
actors: Record<string, Actor>;
createdActors: Record<string, number>;
destroyedActors: Record<string, number>;
boxes: Box[];
messages: Message[];
notes: Note[];
sequenceNumbersEnabled: boolean;
wrapEnabled?: boolean;
currentBox?: Box;
lastCreated?: Actor;
lastDestroyed?: Actor;
}
const state = new ImperativeState<SequenceState>(() => ({
prevActor: undefined,
actors: {},
createdActors: {},
@@ -27,7 +43,7 @@ const state = new ImperativeState(() => ({
lastDestroyed: undefined,
}));
export const addBox = function (data) {
export const addBox = function (data: { text: string; color: string; wrap: boolean }) {
state.records.boxes.push({
name: data.text,
wrap: (data.wrap === undefined && autoWrap()) || !!data.wrap,
@@ -37,20 +53,19 @@ export const addBox = function (data) {
state.records.currentBox = state.records.boxes.slice(-1)[0];
};
export const addActor = function (id, name, description, type) {
export const addActor = function (
id: string,
name: string,
description: { text: string; wrap?: boolean | null; type: string },
type: string
) {
let assignedBox = state.records.currentBox;
const old = state.records.actors[id];
if (old) {
// If already set and trying to set to a new one throw error
if (state.records.currentBox && old.box && state.records.currentBox !== old.box) {
throw new Error(
'A same participant should only be defined in one Box: ' +
old.name +
" can't be in '" +
old.box.name +
"' and in '" +
state.records.currentBox.name +
"' at the same time."
`A same participant should only be defined in one Box: ${old.name} can't be in '${old.box.name}' and in '${state.records.currentBox.name}' at the same time.`
);
}
@@ -82,7 +97,7 @@ export const addActor = function (id, name, description, type) {
properties: {},
actorCnt: null,
rectData: null,
type: type || 'participant',
type: type ?? 'participant',
};
if (state.records.prevActor && state.records.actors[state.records.prevActor]) {
state.records.actors[state.records.prevActor].nextActor = id;
@@ -94,19 +109,23 @@ export const addActor = function (id, name, description, type) {
state.records.prevActor = id;
};
const activationCount = (part) => {
const activationCount = (part: string) => {
let i;
let count = 0;
if (!part) {
return 0;
}
for (i = 0; i < state.records.messages.length; i++) {
if (
state.records.messages[i].type === LINETYPE.ACTIVE_START &&
state.records.messages[i].from.actor === part
state.records.messages[i].from?.actor === part
) {
count++;
}
if (
state.records.messages[i].type === LINETYPE.ACTIVE_END &&
state.records.messages[i].from.actor === part
state.records.messages[i].from?.actor === part
) {
count--;
}
@@ -114,7 +133,12 @@ const activationCount = (part) => {
return count;
};
export const addMessage = function (idFrom, idTo, message, answer) {
export const addMessage = function (
idFrom: Message['from'],
idTo: Message['to'],
message: { text: string; wrap?: boolean },
answer: Message['answer']
) {
state.records.messages.push({
from: idFrom,
to: idTo,
@@ -125,17 +149,21 @@ export const addMessage = function (idFrom, idTo, message, answer) {
};
export const addSignal = function (
idFrom,
idTo,
message = { text: undefined, wrap: undefined },
messageType,
activate = false
idFrom?: Message['from'],
idTo?: Message['to'],
message?: { text: string; wrap: boolean },
messageType?: number,
activate: boolean = false
) {
if (messageType === LINETYPE.ACTIVE_END) {
const cnt = activationCount(idFrom.actor);
const cnt = activationCount(idFrom?.actor || '');
if (cnt < 1) {
// Bail out as there is an activation signal from an inactive participant
let error = new Error('Trying to inactivate an inactive participant (' + idFrom.actor + ')');
const error = new Error(
'Trying to inactivate an inactive participant (' + idFrom?.actor + ')'
);
// @ts-ignore: we are passing hash param to the error object, however we should define our own custom error class to make it type safe
error.hash = {
text: '->>-',
token: '->>-',
@@ -149,8 +177,8 @@ export const addSignal = function (
state.records.messages.push({
from: idFrom,
to: idTo,
message: message.text,
wrap: (message.wrap === undefined && autoWrap()) || !!message.wrap,
message: message?.text ?? '',
wrap: (message?.wrap === undefined && autoWrap()) || !!message?.wrap,
type: messageType,
activate,
});
@@ -181,7 +209,7 @@ export const getCreatedActors = function () {
export const getDestroyedActors = function () {
return state.records.destroyedActors;
};
export const getActor = function (id) {
export const getActor = function (id: string) {
return state.records.actors[id];
};
export const getActorKeys = function () {
@@ -195,7 +223,7 @@ export const disableSequenceNumbers = function () {
};
export const showSequenceNumbers = () => state.records.sequenceNumbersEnabled;
export const setWrap = function (wrapSetting) {
export const setWrap = function (wrapSetting?: boolean) {
state.records.wrapEnabled = wrapSetting;
};
@@ -205,7 +233,7 @@ export const autoWrap = () => {
if (state.records.wrapEnabled !== undefined) {
return state.records.wrapEnabled;
}
return getConfig().sequence.wrap;
return getConfig()?.sequence?.wrap;
};
export const clear = function () {
@@ -213,25 +241,25 @@ export const clear = function () {
commonClear();
};
export const parseMessage = function (str) {
const _str = str.trim();
export const parseMessage = function (str: string) {
const trimmedStr = str.trim();
const message = {
text: _str.replace(/^:?(?:no)?wrap:/, '').trim(),
text: trimmedStr.replace(/^:?(?:no)?wrap:/, '').trim(),
wrap:
_str.match(/^:?wrap:/) !== null
trimmedStr.match(/^:?wrap:/) !== null
? true
: _str.match(/^:?nowrap:/) !== null
: trimmedStr.match(/^:?nowrap:/) !== null
? false
: undefined,
};
log.debug('parseMessage:', message);
log.debug(`parseMessage: ${message}`);
return message;
};
// We expect the box statement to be color first then description
// The color can be rgb,rgba,hsl,hsla, or css code names #hex codes are not supported for now because of the way the char # is handled
// We extract first segment as color, the rest of the line is considered as text
export const parseBoxData = function (str) {
export const parseBoxData = function (str: string) {
const match = str.match(/^((?:rgba?|hsla?)\s*\(.*\)|\w*)(.*)$/);
let color = match != null && match[1] ? match[1].trim() : 'transparent';
let title = match != null && match[2] ? match[2].trim() : undefined;
@@ -312,18 +340,21 @@ export const PLACEMENT = {
OVER: 2,
};
export const addNote = function (actor, placement, message) {
const note = {
export const addNote = function (
actor: { actor: string },
placement: Message['placement'],
message: { text: string; wrap?: boolean }
) {
const note: Note = {
actor: actor,
placement: placement,
message: message.text,
wrap: (message.wrap === undefined && autoWrap()) || !!message.wrap,
};
// Coerce actor into a [to, from, ...] array
//@ts-ignore: Coerce actor into a [to, from, ...] array
// eslint-disable-next-line unicorn/prefer-spread
const actors = [].concat(actor, actor);
state.records.notes.push(note);
state.records.messages.push({
from: actors[0],
@@ -335,7 +366,7 @@ export const addNote = function (actor, placement, message) {
});
};
export const addLinks = function (actorId, text) {
export const addLinks = function (actorId: string, text: { text: string }) {
// find the actor
const actor = getActor(actorId);
// JSON.parse the text
@@ -351,17 +382,17 @@ export const addLinks = function (actorId, text) {
}
};
export const addALink = function (actorId, text) {
export const addALink = function (actorId: string, text: { text: string }) {
// find the actor
const actor = getActor(actorId);
try {
const links = {};
const links: Record<string, string> = {};
let sanitizedText = sanitizeText(text.text, getConfig());
var sep = sanitizedText.indexOf('@');
const sep = sanitizedText.indexOf('@');
sanitizedText = sanitizedText.replace(/&amp;/g, '&');
sanitizedText = sanitizedText.replace(/&equals;/g, '=');
var label = sanitizedText.slice(0, sep - 1).trim();
var link = sanitizedText.slice(sep + 1).trim();
const label = sanitizedText.slice(0, sep - 1).trim();
const link = sanitizedText.slice(sep + 1).trim();
links[label] = link;
// add the deserialized text to the actor's links field.
@@ -372,26 +403,26 @@ export const addALink = function (actorId, text) {
};
/**
* @param {any} actor
* @param {any} links
* @param actor - the actor to add the links to
* @param links - the links to add to the actor
*/
function insertLinks(actor, links) {
function insertLinks(actor: Actor, links: Record<string, string>) {
if (actor.links == null) {
actor.links = links;
} else {
for (let key in links) {
for (const key in links) {
actor.links[key] = links[key];
}
}
}
export const addProperties = function (actorId, text) {
export const addProperties = function (actorId: string, text: { text: string }) {
// find the actor
const actor = getActor(actorId);
// JSON.parse the text
try {
let sanitizedText = sanitizeText(text.text, getConfig());
const properties = JSON.parse(sanitizedText);
const sanitizedText = sanitizeText(text.text, getConfig());
const properties: Record<string, unknown> = JSON.parse(sanitizedText);
// add the deserialized text to the actor's property field.
insertProperties(actor, properties);
} catch (e) {
@@ -400,30 +431,27 @@ export const addProperties = function (actorId, text) {
};
/**
* @param {any} actor
* @param {any} properties
* @param actor - the actor to add the properties to
* @param properties - the properties to add to the actor's properties
*/
function insertProperties(actor, properties) {
function insertProperties(actor: Actor, properties: Record<string, unknown>) {
if (actor.properties == null) {
actor.properties = properties;
} else {
for (let key in properties) {
for (const key in properties) {
actor.properties[key] = properties[key];
}
}
}
/**
*
*/
function boxEnd() {
state.records.currentBox = undefined;
}
export const addDetails = function (actorId, text) {
export const addDetails = function (actorId: string, text: { text: string }) {
// find the actor
const actor = getActor(actorId);
const elem = document.getElementById(text.text);
const elem = document.getElementById(text.text)!;
// JSON.parse the text
try {
@@ -442,7 +470,7 @@ export const addDetails = function (actorId, text) {
}
};
export const getActorProperty = function (actor, key) {
export const getActorProperty = function (actor: Actor, key: string) {
if (actor !== undefined && actor.properties !== undefined) {
return actor.properties[key];
}
@@ -450,20 +478,7 @@ export const getActorProperty = function (actor, key) {
return undefined;
};
/**
* @typedef {object} AddMessageParams A message from one actor to another.
* @property {string} from - The id of the actor sending the message.
* @property {string} to - The id of the actor receiving the message.
* @property {string} msg - The message text.
* @property {number} signalType - The type of signal.
* @property {"addMessage"} type - Set to `"addMessage"` if this is an `AddMessageParams`.
* @property {boolean} [activate] - If `true`, this signal starts an activation.
*/
/**
* @param {object | object[] | AddMessageParams} param - Object of parameters.
*/
export const apply = function (param) {
export const apply = function (param: any | AddMessageParams | AddMessageParams[]) {
if (Array.isArray(param)) {
param.forEach(function (item) {
apply(item);

View File

@@ -0,0 +1,92 @@
export interface Box {
name: string;
wrap: boolean;
fill: string;
actorKeys: string[];
}
export interface Actor {
box?: Box;
name: string;
description: string;
wrap: boolean;
prevActor?: string;
nextActor?: string;
links: Record<string, unknown>;
properties: Record<string, unknown>;
actorCnt: number | null;
rectData: unknown;
type: string;
}
export interface Message {
from?: { actor: string };
to?: { actor: string };
message:
| string
| {
start: number;
step: number;
visible: boolean;
};
wrap: boolean;
answer?: unknown;
type?: number;
activate?: boolean;
placement?: string;
}
export interface AddMessageParams {
from: string;
to: string;
msg: string;
signalType: number;
type:
| 'addMessage'
| 'sequenceIndex'
| 'addParticipant'
| 'createParticipant'
| 'destroyParticipant'
| 'activeStart'
| 'activeEnd'
| 'addNote'
| 'addLinks'
| 'addALink'
| 'addProperties'
| 'addDetails'
| 'boxStart'
| 'boxEnd'
| 'loopStart'
| 'loopEnd'
| 'rectStart'
| 'rectEnd'
| 'optStart'
| 'optEnd'
| 'altStart'
| 'else'
| 'altEnd'
| 'setAccTitle'
| 'parStart'
| 'parAnd'
| 'parEnd'
| 'and'
| 'criticalStart'
| 'criticalOption'
| 'option'
| 'criticalEnd'
| 'breakStart'
| 'breakEnd'
| 'parOverStart'
| 'parOverEnd'
| 'parOverAnd'
| 'parOverEnd';
activate: boolean;
}
export interface Note {
actor: { actor: string };
placement: Message['placement'];
message: string;
wrap: boolean;
}

View File

@@ -2,11 +2,11 @@
* Resettable state storage.
* @example
* ```
* const state = new ImperativeState(() => {
* const state = new ImperativeState(() => ({
* foo: undefined as string | undefined,
* bar: [] as number[],
* baz: 1 as number | undefined,
* });
* }));
*
* state.records.foo = "hi";
* console.log(state.records.foo); // prints "hi";
@@ -21,7 +21,7 @@
* // }
* ```
*/
export class ImperativeState<S extends Record<string, unknown>> {
export class ImperativeState<S> {
public records: S;
/**