From da30cc85bcca8e746b6c46d05f5757430b169151 Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sat, 14 Feb 2026 02:11:34 +0530 Subject: [PATCH 01/10] refactor(core): removed redundant existence checks --- packages/core/eslint.config.mjs | 10 + packages/core/package.json | 5 +- packages/core/src/codecs/octetstream-codec.ts | 48 +- packages/core/src/consumed-thing.ts | 90 ++-- packages/core/src/content-serdes.ts | 49 +- packages/core/src/exposed-thing.ts | 429 ++++++++---------- packages/core/src/helpers.ts | 15 +- packages/core/src/protocol-helpers.ts | 87 ++-- .../core/src/protocol-listener-registry.ts | 5 - packages/core/src/serdes.ts | 23 +- packages/core/src/servient.ts | 2 +- packages/core/test/content-serdes-test.ts | 2 +- packages/core/test/server-test.ts | 2 +- 13 files changed, 308 insertions(+), 459 deletions(-) create mode 100644 packages/core/eslint.config.mjs diff --git a/packages/core/eslint.config.mjs b/packages/core/eslint.config.mjs new file mode 100644 index 000000000..cffb236c9 --- /dev/null +++ b/packages/core/eslint.config.mjs @@ -0,0 +1,10 @@ +import rootConfig from '../../eslint.config.mjs'; + +export default [ + ...rootConfig, + { + rules: { + '@typescript-eslint/no-unnecessary-condition': 'warn' + } + } +]; diff --git a/packages/core/package.json b/packages/core/package.json index 588160be5..76b0a0b7f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -50,8 +50,5 @@ "directories": { "test": "test" }, - "keywords": [], - "eslintConfig": { - "extends": "../../.eslintrc.js" - } + "keywords": [] } diff --git a/packages/core/src/codecs/octetstream-codec.ts b/packages/core/src/codecs/octetstream-codec.ts index 5aec25931..86ffadaaf 100644 --- a/packages/core/src/codecs/octetstream-codec.ts +++ b/packages/core/src/codecs/octetstream-codec.ts @@ -108,7 +108,7 @@ export default class OctetstreamCodec implements ContentCodec { if (typeSem) { if (typeSem[1] === "u") { // compare with schema information - if (parameters?.signed === "true") { + if (parameters.signed === "true") { throw new Error("Type is unsigned but 'signed' is true"); } // no schema, but type is unsigned @@ -117,7 +117,7 @@ export default class OctetstreamCodec implements ContentCodec { dataType = typeSem[2]; if (parseInt(typeSem[3]) !== bitLength) { throw new Error( - `Type is '${(typeSem[1] ?? "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + bitLength + `Type is '${(typeSem[1] || "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + bitLength ); } } @@ -130,11 +130,11 @@ export default class OctetstreamCodec implements ContentCodec { } // Handle byte swapping - if (parameters?.byteSeq?.includes("BYTE_SWAP") === true && bytes.length > 1) { + if (parameters.byteSeq?.includes("BYTE_SWAP") === true && bytes.length > 1) { bytes.swap16(); } - if (offset !== undefined && bitLength < bytes.length * 8) { + if (bitLength < bytes.length * 8) { bytes = this.readBits(bytes, offset, bitLength); bitLength = bytes.length * 8; } @@ -183,8 +183,8 @@ export default class OctetstreamCodec implements ContentCodec { ? bytes.readInt16BE(0) : bytes.readUInt16BE(0) : signed - ? bytes.readInt16LE(0) - : bytes.readUInt16LE(0); + ? bytes.readInt16LE(0) + : bytes.readUInt16LE(0); case 32: return bigEndian @@ -192,8 +192,8 @@ export default class OctetstreamCodec implements ContentCodec { ? bytes.readInt32BE(0) : bytes.readUInt32BE(0) : signed - ? bytes.readInt32LE(0) - : bytes.readUInt32LE(0); + ? bytes.readInt32LE(0) + : bytes.readUInt32LE(0); default: { const result = bigEndian @@ -201,8 +201,8 @@ export default class OctetstreamCodec implements ContentCodec { ? bytes.readIntBE(0, dataLength / 8) : bytes.readUIntBE(0, dataLength / 8) : signed - ? bytes.readIntLE(0, dataLength / 8) - : bytes.readUIntLE(0, dataLength / 8); + ? bytes.readIntLE(0, dataLength / 8) + : bytes.readUIntLE(0, dataLength / 8); // warn about numbers being too big to be represented as safe integers if (!Number.isSafeInteger(result)) { warn("Result is not a safe integer"); @@ -282,7 +282,7 @@ export default class OctetstreamCodec implements ContentCodec { throw new Error("'ex:bitOffset' must be a non-negative number"); } - let dataType: string = schema?.type ?? undefined; + let dataType: string | undefined = schema?.type; if (value === undefined) { throw new Error("Undefined value"); @@ -300,7 +300,7 @@ export default class OctetstreamCodec implements ContentCodec { if (typeSem) { if (typeSem[1] === "u") { // compare with schema information - if (parameters?.signed === "true") { + if (parameters.signed === "true") { throw new Error("Type is unsigned but 'signed' is true"); } // no schema, but type is unsigned @@ -310,8 +310,8 @@ export default class OctetstreamCodec implements ContentCodec { if (bitLength !== undefined) { if (parseInt(typeSem[3]) !== bitLength) { throw new Error( - `Type is '${(typeSem[1] ?? "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + - bitLength + `Type is '${(typeSem[1] || "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + + bitLength ); } } else { @@ -430,10 +430,10 @@ export default class OctetstreamCodec implements ContentCodec { if (value < 0 || value > limit) { throw new Error( "Integer overflow when representing " + - value + - " as an unsigned integer using " + - length + - " bit(s)" + value + + " as an unsigned integer using " + + length + + " bit(s)" ); } } @@ -446,7 +446,7 @@ export default class OctetstreamCodec implements ContentCodec { } // Handle byte swapping - if (byteSeq?.includes("BYTE_SwAP") && byteLength > 1) { + if (byteSeq.includes("BYTE_SwAP") && byteLength > 1) { buf.swap16(); } switch (byteLength) { @@ -460,8 +460,8 @@ export default class OctetstreamCodec implements ContentCodec { ? buf.writeInt16BE(value, 0) : buf.writeUInt16BE(value, 0) : signed - ? buf.writeInt16LE(value, 0) - : buf.writeUInt16LE(value, 0); + ? buf.writeInt16LE(value, 0) + : buf.writeUInt16LE(value, 0); break; case 4: @@ -470,8 +470,8 @@ export default class OctetstreamCodec implements ContentCodec { ? buf.writeInt32BE(value, 0) : buf.writeUInt32BE(value, 0) : signed - ? buf.writeInt32LE(value, 0) - : buf.writeUInt32LE(value, 0); + ? buf.writeInt32LE(value, 0) + : buf.writeUInt32LE(value, 0); break; default: @@ -584,7 +584,7 @@ export default class OctetstreamCodec implements ContentCodec { parameters: { [key: string]: string | undefined } = {}, result?: Buffer | undefined ): Buffer { - if (typeof value !== "object" || value === null) { + if (typeof value !== "object") { throw new Error("Value is not an object"); } diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index a17c72a2c..b9007ee61 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -130,8 +130,8 @@ class InternalPropertySubscription extends InternalSubscription { private readonly form: FormElementProperty ) { super(thing, name, client); - const index = this.thing.properties?.[name].forms.indexOf(form as Form); - if (index === undefined || index < 0) { + const index = this.thing.properties[name].forms.indexOf(form as Form); + if (index < 0) { throw new Error(`Could not find form ${form.href} in property ${name}`); } this.formIndex = index; @@ -144,9 +144,6 @@ class InternalPropertySubscription extends InternalSubscription { public async unobserveProperty(options: WoT.InteractionOptions = {}): Promise { const tp = this.thing.properties[this.name]; - if (tp == null) { - throw new Error(`ConsumedThing '${this.thing.title}' does not have property ${this.name}`); - } options.formIndex ??= this.matchingUnsubscribeForm(); const { form } = this.thing.getClientFor(tp.forms, "unobserveproperty", Affordance.PropertyAffordance, options); if (form == null) { @@ -195,7 +192,7 @@ class InternalPropertySubscription extends InternalSubscription { for (let i = 0; i < forms.length; i++) { let score = 0; const form = forms[i]; - if (form.op === operation || (form?.op?.includes(operation) === true && Array.isArray(form.op) === true)) { + if (form.op === operation || (form.op?.includes(operation) === true && Array.isArray(form.op) === true)) { score += 1; } @@ -232,7 +229,7 @@ function findFormIndexWithScoring( for (let i = 0; i < forms.length; i++) { let score = 0; const form = forms[i]; - if (form.op === operation || (form?.op?.includes(operation) === true && Array.isArray(form.op) === true)) { + if (form.op === operation || (form.op?.includes(operation) === true && Array.isArray(form.op) === true)) { score += 1; } @@ -261,8 +258,8 @@ class InternalEventSubscription extends InternalSubscription { private readonly form: FormElementEvent ) { super(thing, name, client); - const index = this.thing.events?.[name].forms.indexOf(form as Form); - if (index === undefined || index < 0) { + const index = this.thing.events[name].forms.indexOf(form as Form); + if (index < 0) { throw new Error(`Could not find form ${form.href} in event ${name}`); } this.formIndex = index; @@ -275,9 +272,6 @@ class InternalEventSubscription extends InternalSubscription { public async unsubscribeEvent(options: WoT.InteractionOptions = {}): Promise { const te = this.thing.events[this.name]; - if (te == null) { - throw new Error(`ConsumedThing '${this.thing.title}' does not have event ${this.name}`); - } options.formIndex ??= this.matchingUnsubscribeForm(); @@ -461,7 +455,7 @@ export default class ConsumedThing extends Thing implements IConsumedThing { let ws: SecurityScheme | undefined = this.securityDefinitions[s]; // also push nosec in case of proxy - if (ws?.scheme === "combo") { + if (ws.scheme === "combo") { ws = resolveComboScheme(ws as ComboSecurityScheme, s); } if (ws != null) { @@ -476,23 +470,21 @@ export default class ConsumedThing extends Thing implements IConsumedThing { } ensureClientSecurity(client: ProtocolClient, form: Form | undefined): void { - if (this.securityDefinitions != null) { - const logStatement = () => - debug(`ConsumedThing '${this.title}' setting credentials for ${client} based on thing security`); - - if (form != null && Array.isArray(form.security) && form.security.length > 0) { - // Note security member in form objects overrides (i.e., completely replace) all definitions activated at the Thing level - // see https://www.w3.org/TR/wot-thing-description/#security-serialization-json - - logStatement(); - client.setSecurity(this.getSecuritySchemes(form.security), this.#servient.retrieveCredentials(this.id)); - } else if (Array.isArray(this.security) && this.security.length > 0) { - logStatement(); - client.setSecurity( - this.getSecuritySchemes(this.security as string[]), - this.#servient.getCredentials(this.id) - ); - } + const logStatement = () => + debug(`ConsumedThing '${this.title}' setting credentials for ${client} based on thing security`); + + if (form != null && Array.isArray(form.security) && form.security.length > 0) { + // Note security member in form objects overrides (i.e., completely replace) all definitions activated at the Thing level + // see https://www.w3.org/TR/wot-thing-description/#security-serialization-json + + logStatement(); + client.setSecurity(this.getSecuritySchemes(form.security), this.#servient.retrieveCredentials(this.id)); + } else if (Array.isArray(this.security) && this.security.length > 0) { + logStatement(); + client.setSecurity( + this.getSecuritySchemes(this.security as string[]), + this.#servient.getCredentials(this.id) + ); } } @@ -566,17 +558,12 @@ export default class ConsumedThing extends Thing implements IConsumedThing { async readProperty(propertyName: string, options?: WoT.InteractionOptions): Promise { // TODO pass expected form op to getClientFor() const tp = this.properties[propertyName]; - if (tp == null) { - throw new Error(`ConsumedThing '${this.title}' does not have property ${propertyName}`); - } const { client, form } = this.getClientFor(tp.forms, "readproperty", Affordance.PropertyAffordance, options); if (form == null) { throw new Error(`ConsumedThing '${this.title}' did not get suitable form`); } - if (client == null) { - throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`); - } + debug(`ConsumedThing '${this.title}' reading ${form.href}`); // uriVariables ? @@ -597,7 +584,7 @@ export default class ConsumedThing extends Thing implements IConsumedThing { outputDataSchema: WoT.DataSchema | undefined ): InteractionOutput { // infer media type from form if not in response metadata - content.type ??= form.contentType ?? "application/json"; + content.type = form.contentType ?? "application/json"; // check if returned media type is the same as expected media type (from TD) this.checkMediaTypeOrThrow(content, form); return new InteractionOutput(content, form, outputDataSchema); @@ -623,7 +610,7 @@ export default class ConsumedThing extends Thing implements IConsumedThing { synchronous?: boolean ): ActionInteractionOutput { // infer media type from form if not in response metadata - content.type ??= form.contentType ?? "application/json"; + content.type = form.contentType ?? "application/json"; // check if returned media type is the same as expected media type (from TD) this.checkMediaTypeOrThrow(content, form); return new ActionInteractionOutput(content, form, outputDataSchema, synchronous); @@ -677,16 +664,10 @@ export default class ConsumedThing extends Thing implements IConsumedThing { ): Promise { // TODO pass expected form op to getClientFor() const tp = this.properties[propertyName]; - if (tp == null) { - throw new Error(`ConsumedThing '${this.title}' does not have property ${propertyName}`); - } const { client, form } = this.getClientFor(tp.forms, "writeproperty", Affordance.PropertyAffordance, options); if (form == null) { throw new Error(`ConsumedThing '${this.title}' did not get suitable form`); } - if (client == null) { - throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`); - } debug(`ConsumedThing '${this.title}' writing ${form.href} with '${value}'`); const content = ContentManager.valueToContent(value, tp, form.contentType); @@ -718,19 +699,12 @@ export default class ConsumedThing extends Thing implements IConsumedThing { options?: WoT.InteractionOptions ): Promise { const ta = this.actions[actionName]; - if (ta == null) { - throw new Error(`ConsumedThing '${this.title}' does not have action ${actionName}`); - } const { client, form } = this.getClientFor(ta.forms, "invokeaction", Affordance.ActionAffordance, options); if (form == null) { throw new Error(`ConsumedThing '${this.title}' did not get suitable form`); } - if (client == null) { - throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`); - } debug( - `ConsumedThing '${this.title}' invoking ${form.href}${ - parameter !== undefined ? " with '" + parameter + "'" : "" + `ConsumedThing '${this.title}' invoking ${form.href}${parameter !== undefined ? " with '" + parameter + "'" : "" }` ); @@ -763,16 +737,10 @@ export default class ConsumedThing extends Thing implements IConsumedThing { options?: WoT.InteractionOptions ): Promise { const tp = this.properties[name]; - if (tp == null) { - throw new Error(`ConsumedThing '${this.title}' does not have property ${name}`); - } const { client, form } = this.getClientFor(tp.forms, "observeproperty", Affordance.PropertyAffordance, options); if (form == null) { throw new Error(`ConsumedThing '${this.title}' did not get suitable form`); } - if (client == null) { - throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`); - } if (this.observedProperties.has(name)) { throw new Error( `ConsumedThing '${this.title}' has already a function subscribed to ${name}. You can only observe once` @@ -820,16 +788,10 @@ export default class ConsumedThing extends Thing implements IConsumedThing { options?: WoT.InteractionOptions ): Promise { const te = this.events[name]; - if (te == null) { - throw new Error(`ConsumedThing '${this.title}' does not have event ${name}`); - } const { client, form } = this.getClientFor(te.forms, "subscribeevent", Affordance.EventAffordance, options); if (form == null) { throw new Error(`ConsumedThing '${this.title}' did not get suitable form`); } - if (client == null) { - throw new Error(`ConsumedThing '${this.title}' did not get suitable client for ${form.href}`); - } if (this.subscribedEvents.has(name)) { throw new Error( `ConsumedThing '${this.title}' has already a function subscribed to ${name}. You can only subscribe once` diff --git a/packages/core/src/content-serdes.ts b/packages/core/src/content-serdes.ts index d6cac1189..c16800928 100644 --- a/packages/core/src/content-serdes.ts +++ b/packages/core/src/content-serdes.ts @@ -57,29 +57,28 @@ export class ContentSerdes { private offered: Set = new Set(); public static get(): ContentSerdes { - if (this.instance == null) { - this.instance = new ContentSerdes(); - // JSON - this.instance.addCodec(new JsonCodec(), true); - this.instance.addCodec(new JsonCodec("application/senml+json")); - this.instance.addCodec(new JsonCodec("application/td+json")); - this.instance.addCodec(new JsonCodec("application/ld+json")); - // CBOR - this.instance.addCodec(new CborCodec(), true); - // Text - this.instance.addCodec(new TextCodec()); - this.instance.addCodec(new TextCodec("text/html")); - this.instance.addCodec(new TextCodec("text/css")); - this.instance.addCodec(new TextCodec("application/xml")); - this.instance.addCodec(new TextCodec("application/xhtml+xml")); - this.instance.addCodec(new TextCodec("image/svg+xml")); - // Base64 - this.instance.addCodec(new Base64Codec("image/png")); - this.instance.addCodec(new Base64Codec("image/gif")); - this.instance.addCodec(new Base64Codec("image/jpeg")); - // OctetStream - this.instance.addCodec(new OctetstreamCodec()); - } + this.instance = new ContentSerdes(); + // JSON + this.instance.addCodec(new JsonCodec(), true); + this.instance.addCodec(new JsonCodec("application/senml+json")); + this.instance.addCodec(new JsonCodec("application/td+json")); + this.instance.addCodec(new JsonCodec("application/ld+json")); + // CBOR + this.instance.addCodec(new CborCodec(), true); + // Text + this.instance.addCodec(new TextCodec()); + this.instance.addCodec(new TextCodec("text/html")); + this.instance.addCodec(new TextCodec("text/css")); + this.instance.addCodec(new TextCodec("application/xml")); + this.instance.addCodec(new TextCodec("application/xhtml+xml")); + this.instance.addCodec(new TextCodec("image/svg+xml")); + // Base64 + this.instance.addCodec(new Base64Codec("image/png")); + this.instance.addCodec(new Base64Codec("image/gif")); + this.instance.addCodec(new Base64Codec("image/jpeg")); + // OctetStream + this.instance.addCodec(new OctetstreamCodec()); + return this.instance; } @@ -126,7 +125,6 @@ export class ContentSerdes { } public contentToValue(content: ReadContent, schema: DataSchema): DataSchemaValue | undefined { - if (content.type === undefined) { if (content.body.byteLength > 0) { // default to application/json content.type = ContentSerdes.DEFAULT; @@ -134,7 +132,6 @@ export class ContentSerdes { // empty payload without media type -> void/undefined (note: e.g., empty payload with text/plain -> "") return undefined; } - } // split into media type and parameters const mt = ContentSerdes.getMediaType(content.type); @@ -162,8 +159,6 @@ export class ContentSerdes { schema: DataSchema | undefined, contentType = ContentSerdes.DEFAULT ): Content { - if (value === undefined) warn("ContentSerdes valueToContent got no value"); - if (value instanceof ReadableStream) { return new Content(contentType, ProtocolHelpers.toNodeStream(value)); } diff --git a/packages/core/src/exposed-thing.ts b/packages/core/src/exposed-thing.ts index 51401068d..7283f5ea7 100644 --- a/packages/core/src/exposed-thing.ts +++ b/packages/core/src/exposed-thing.ts @@ -152,32 +152,22 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { } public emitEvent(name: string, data: WoT.InteractionInput): void { - if (this.events[name] != null) { - const eventAffordance = this.events[name]; - this.#eventListeners.notify(eventAffordance, data, eventAffordance.data); - } else { - // NotFoundError - throw new Error("NotFoundError for event '" + name + "'"); - } + const eventAffordance = this.events[name]; + this.#eventListeners.notify(eventAffordance, data, eventAffordance.data); } public async emitPropertyChange(name: string): Promise { - if (this.properties[name] != null) { - const property = this.properties[name]; - const readHandler = this.#propertyHandlers.get(name)?.readHandler; + const property = this.properties[name]; + const readHandler = this.#propertyHandlers.get(name)?.readHandler; - if (!readHandler) { - throw new Error( - "Can't read property readHandler is not defined. Did you forget to register a readHandler?" - ); - } - - const data = await readHandler(); - this.#propertyListeners.notify(property, data, property); - } else { - // NotFoundError - throw new Error("NotFoundError for property '" + name + "'"); + if (!readHandler) { + throw new Error( + "Can't read property readHandler is not defined. Did you forget to register a readHandler?" + ); } + + const data = await readHandler(); + this.#propertyListeners.notify(property, data, property); } /** @inheritDoc */ @@ -211,24 +201,20 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { setPropertyReadHandler(propertyName: string, handler: WoT.PropertyReadHandler): WoT.ExposedThing { debug(`ExposedThing '${this.title}' setting read handler for '${propertyName}'`); - if (this.properties[propertyName] != null) { - // setting read handler for writeOnly not allowed - if (this.properties[propertyName].writeOnly === true) { - throw new Error( - `ExposedThing '${this.title}' cannot set read handler for property '${propertyName}' due to writeOnly flag` - ); + // setting read handler for writeOnly not allowed + if (this.properties[propertyName].writeOnly === true) { + throw new Error( + `ExposedThing '${this.title}' cannot set read handler for property '${propertyName}' due to writeOnly flag` + ); + } else { + let propertyHandler = this.#propertyHandlers.get(propertyName); + if (propertyHandler) { + propertyHandler.readHandler = handler; } else { - let propertyHandler = this.#propertyHandlers.get(propertyName); - if (propertyHandler) { - propertyHandler.readHandler = handler; - } else { - propertyHandler = { readHandler: handler }; - } - - this.#propertyHandlers.set(propertyName, propertyHandler); + propertyHandler = { readHandler: handler }; } - } else { - throw new Error(`ExposedThing '${this.title}' has no Property '${propertyName}'`); + + this.#propertyHandlers.set(propertyName, propertyHandler); } return this; } @@ -236,24 +222,21 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { /** @inheritDoc */ setPropertyWriteHandler(propertyName: string, handler: WoT.PropertyWriteHandler): WoT.ExposedThing { debug(`ExposedThing '${this.title}' setting write handler for '${propertyName}'`); - if (this.properties[propertyName] != null) { - // setting write handler for readOnly not allowed - if (this.properties[propertyName].readOnly === true) { - throw new Error( - `ExposedThing '${this.title}' cannot set write handler for property '${propertyName}' due to readOnly flag` - ); - } else { - let propertyHandler = this.#propertyHandlers.get(propertyName); - if (propertyHandler) { - propertyHandler.writeHandler = handler; - } else { - propertyHandler = { writeHandler: handler }; - } - this.#propertyHandlers.set(propertyName, propertyHandler); - } + // setting write handler for readOnly not allowed + if (this.properties[propertyName].readOnly === true) { + throw new Error( + `ExposedThing '${this.title}' cannot set write handler for property '${propertyName}' due to readOnly flag` + ); } else { - throw new Error(`ExposedThing '${this.title}' has no Property '${propertyName}'`); + let propertyHandler = this.#propertyHandlers.get(propertyName); + if (propertyHandler) { + propertyHandler.writeHandler = handler; + } else { + propertyHandler = { writeHandler: handler }; + } + + this.#propertyHandlers.set(propertyName, propertyHandler); } return this; } @@ -262,22 +245,18 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { setPropertyObserveHandler(name: string, handler: WoT.PropertyReadHandler): WoT.ExposedThing { debug(`ExposedThing '${this.title}' setting property observe handler for '${name}'`); - if (this.properties[name] != null) { - if (this.properties[name].observable !== true) { - throw new Error( - `ExposedThing '${this.title}' cannot set observe handler for property '${name}' since the observable flag is set to false` - ); + if (this.properties[name].observable !== true) { + throw new Error( + `ExposedThing '${this.title}' cannot set observe handler for property '${name}' since the observable flag is set to false` + ); + } else { + let propertyHandler = this.#propertyHandlers.get(name); + if (propertyHandler) { + propertyHandler.observeHandler = handler; } else { - let propertyHandler = this.#propertyHandlers.get(name); - if (propertyHandler) { - propertyHandler.observeHandler = handler; - } else { - propertyHandler = { observeHandler: handler }; - } - this.#propertyHandlers.set(name, propertyHandler); + propertyHandler = { observeHandler: handler }; } - } else { - throw new Error(`ExposedThing '${this.title}' has no Property '${name}'`); + this.#propertyHandlers.set(name, propertyHandler); } return this; } @@ -286,22 +265,18 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { setPropertyUnobserveHandler(name: string, handler: WoT.PropertyReadHandler): WoT.ExposedThing { debug(`ExposedThing '${this.title}' setting property unobserve handler for '${name}'`); - if (this.properties[name] != null) { - if (this.properties[name].observable !== true) { - throw new Error( - `ExposedThing '${this.title}' cannot set unobserve handler for property '${name}' due to missing observable flag` - ); + if (this.properties[name].observable !== true) { + throw new Error( + `ExposedThing '${this.title}' cannot set unobserve handler for property '${name}' due to missing observable flag` + ); + } else { + let propertyHandler = this.#propertyHandlers.get(name); + if (propertyHandler) { + propertyHandler.unobserveHandler = handler; } else { - let propertyHandler = this.#propertyHandlers.get(name); - if (propertyHandler) { - propertyHandler.unobserveHandler = handler; - } else { - propertyHandler = { unobserveHandler: handler }; - } - this.#propertyHandlers.set(name, propertyHandler); + propertyHandler = { unobserveHandler: handler }; } - } else { - throw new Error(`ExposedThing '${this.title}' has no Property '${name}'`); + this.#propertyHandlers.set(name, propertyHandler); } return this; } @@ -309,12 +284,7 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { /** @inheritDoc */ setActionHandler(actionName: string, handler: WoT.ActionHandler): WoT.ExposedThing { debug(`ExposedThing '${this.title}' setting action handler for '${actionName}'`); - - if (this.actions[actionName] != null) { - this.#actionHandlers.set(actionName, handler); - } else { - throw new Error(`ExposedThing '${this.title}' has no Action '${actionName}'`); - } + this.#actionHandlers.set(actionName, handler); return this; } @@ -322,18 +292,13 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { setEventSubscribeHandler(name: string, handler: WoT.EventSubscriptionHandler): WoT.ExposedThing { debug(`ExposedThing '${this.title}' setting event subscribe handler for '${name}'`); - if (this.events[name] != null) { - let eventHandler = this.#eventHandlers.get(name); - if (eventHandler) { - eventHandler.subscribe = handler; - } else { - eventHandler = { subscribe: handler }; - } - - this.#eventHandlers.set(name, eventHandler); + let eventHandler = this.#eventHandlers.get(name); + if (eventHandler) { + eventHandler.subscribe = handler; } else { - throw new Error(`ExposedThing '${this.title}' has no Event '${name}'`); + eventHandler = { subscribe: handler }; } + this.#eventHandlers.set(name, eventHandler); return this; } @@ -341,18 +306,13 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { setEventUnsubscribeHandler(name: string, handler: WoT.EventSubscriptionHandler): WoT.ExposedThing { debug(`ExposedThing '${this.title}' setting event unsubscribe handler for '${name}'`); - if (this.events[name] != null) { - let eventHandler = this.#eventHandlers.get(name); - if (eventHandler) { - eventHandler.unsubscribe = handler; - } else { - eventHandler = { unsubscribe: handler }; - } - - this.#eventHandlers.set(name, eventHandler); + let eventHandler = this.#eventHandlers.get(name); + if (eventHandler) { + eventHandler.unsubscribe = handler; } else { - throw new Error(`ExposedThing '${this.title}' has no Event '${name}'`); + eventHandler = { unsubscribe: handler }; } + this.#eventHandlers.set(name, eventHandler); return this; } @@ -366,27 +326,23 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { options: WoT.InteractionOptions & { formIndex: number } ): Promise { // TODO: handling URI variables? - if (this.actions[name] != null) { - debug(`ExposedThing '${this.title}' has Action state of '${name}'`); - - const handler = this.#actionHandlers.get(name); - if (handler != null) { - debug(`ExposedThing '${this.title}' calls registered handler for Action '${name}'`); - Helpers.validateInteractionOptions(this, this.actions[name], options); - const form = this.actions[name].forms[options.formIndex] ?? { contentType: "application/json" }; - const result: WoT.InteractionInput | void = await handler( - new InteractionOutput(inputContent, form, this.actions[name].input), - options - ); - if (result !== undefined) { - // TODO: handle form.response.contentType - return ContentManager.valueToContent(result, this.actions[name].output, form.contentType); - } - } else { - throw new Error(`ExposedThing '${this.title}' has no handler for Action '${name}'`); + debug(`ExposedThing '${this.title}' has Action state of '${name}'`); + + const handler = this.#actionHandlers.get(name); + if (handler != null) { + debug(`ExposedThing '${this.title}' calls registered handler for Action '${name}'`); + Helpers.validateInteractionOptions(this, this.actions[name], options); + const form = this.actions[name].forms[options.formIndex] ?? { contentType: "application/json" }; + const result: WoT.InteractionInput | void = await handler( + new InteractionOutput(inputContent, form, this.actions[name].input), + options + ); + if (result !== undefined) { + // TODO: handle form.response.contentType + return ContentManager.valueToContent(result, this.actions[name].output, form.contentType); } } else { - throw new Error(`ExposedThing '${this.title}', no action found for '${name}'`); + throw new Error(`ExposedThing '${this.title}' has no handler for Action '${name}'`); } } @@ -398,28 +354,24 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { propertyName: string, options: WoT.InteractionOptions & { formIndex: number } ): Promise { - if (this.properties[propertyName] != null) { - debug(`ExposedThing '${this.title}' has Action state of '${propertyName}'`); - - const readHandler = this.#propertyHandlers.get(propertyName)?.readHandler; - - if (readHandler != null) { - debug(`ExposedThing '${this.title}' calls registered readHandler for Property '${propertyName}'`); - Helpers.validateInteractionOptions(this, this.properties[propertyName], options); - const result: WoT.InteractionInput | void = await readHandler(options); - const form = this.properties[propertyName]?.forms[options.formIndex] ?? { - contentType: "application/json", - }; - return ContentManager.valueToContent( - result, - this.properties[propertyName], - form?.contentType ?? "application/json" - ); - } else { - throw new Error(`ExposedThing '${this.title}' has no readHandler for Property '${propertyName}'`); - } + debug(`ExposedThing '${this.title}' has Action state of '${propertyName}'`); + + const readHandler = this.#propertyHandlers.get(propertyName)?.readHandler; + + if (readHandler != null) { + debug(`ExposedThing '${this.title}' calls registered readHandler for Property '${propertyName}'`); + Helpers.validateInteractionOptions(this, this.properties[propertyName], options); + const result: WoT.InteractionInput | void = await readHandler(options); + const form = this.properties[propertyName].forms[options.formIndex] ?? { + contentType: "application/json", + }; + return ContentManager.valueToContent( + result, + this.properties[propertyName], + form.contentType ?? "application/json" + ); } else { - throw new Error(`ExposedThing '${this.title}', no property found for '${propertyName}'`); + throw new Error(`ExposedThing '${this.title}' has no readHandler for Property '${propertyName}'`); } } @@ -483,21 +435,17 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { options: WoT.InteractionOptions & { formIndex: number } ): Promise { // TODO: to be removed next api does not allow an ExposedThing to be also a ConsumeThing - if (this.properties[propertyName] != null) { - if (this.properties[propertyName].readOnly === true) { - throw new Error(`ExposedThing '${this.title}', property '${propertyName}' is readOnly`); - } - Helpers.validateInteractionOptions(this, this.properties[propertyName], options); - const writeHandler = this.#propertyHandlers.get(propertyName)?.writeHandler; - const form = this.properties[propertyName]?.forms[options.formIndex] ?? {}; - // call write handler (if any) - if (writeHandler != null) { - await writeHandler(new InteractionOutput(inputContent, form, this.properties[propertyName]), options); - } else { - throw new Error(`ExposedThing '${this.title}' has no writeHandler for Property '${propertyName}'`); - } + if (this.properties[propertyName].readOnly === true) { + throw new Error(`ExposedThing '${this.title}', property '${propertyName}' is readOnly`); + } + Helpers.validateInteractionOptions(this, this.properties[propertyName], options); + const writeHandler = this.#propertyHandlers.get(propertyName)?.writeHandler; + const form = this.properties[propertyName].forms[options.formIndex] ?? {}; + // call write handler (if any) + if (writeHandler != null) { + await writeHandler(new InteractionOutput(inputContent, form, this.properties[propertyName]), options); } else { - throw new Error(`ExposedThing '${this.title}', no property found for '${propertyName}'`); + throw new Error(`ExposedThing '${this.title}' has no writeHandler for Property '${propertyName}'`); } } @@ -539,33 +487,29 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { listener: ContentListener, options: WoT.InteractionOptions & { formIndex: number } ): Promise { - if (this.events[name] != null) { - Helpers.validateInteractionOptions(this, this.events[name], options); - - const formIndex = ProtocolHelpers.getFormIndexForOperation( - this.events[name], - "event", - "subscribeevent", - options.formIndex - ); + Helpers.validateInteractionOptions(this, this.events[name], options); - if (formIndex !== -1) { - this.#eventListeners.register(this.events[name], formIndex, listener); - debug(`ExposedThing '${this.title}' subscribes to event '${name}'`); - } else { - throw new Error( - `ExposedThing '${this.title}', no property listener from found for '${name}' with form index '${options.formIndex}'` - ); - } + const formIndex = ProtocolHelpers.getFormIndexForOperation( + this.events[name], + "event", + "subscribeevent", + options.formIndex + ); - const subscribe = this.#eventHandlers.get(name)?.subscribe; - if (subscribe) { - await subscribe(options); - } + if (formIndex !== -1) { + this.#eventListeners.register(this.events[name], formIndex, listener); debug(`ExposedThing '${this.title}' subscribes to event '${name}'`); } else { - throw new Error(`ExposedThing '${this.title}', no event found for '${name}'`); + throw new Error( + `ExposedThing '${this.title}', no property listener from found for '${name}' with form index '${options.formIndex}'` + ); + } + + const subscribe = this.#eventHandlers.get(name)?.subscribe; + if (subscribe) { + await subscribe(options); } + debug(`ExposedThing '${this.title}' subscribes to event '${name}'`); } /** @@ -577,30 +521,27 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { listener: ContentListener, options: WoT.InteractionOptions & { formIndex: number } ): void { - if (this.events[name] != null) { - Helpers.validateInteractionOptions(this, this.events[name], options); - - const formIndex = ProtocolHelpers.getFormIndexForOperation( - this.events[name], - "event", - "unsubscribeevent", - options.formIndex - ); - if (formIndex !== -1) { - this.#eventListeners.unregister(this.events[name], formIndex, listener); - } else { - throw new Error( - `ExposedThing '${this.title}', no event listener from found for '${name}' with form index '${options.formIndex}'` - ); - } - const unsubscribe = this.#eventHandlers.get(name)?.unsubscribe; - if (unsubscribe) { - unsubscribe(options); - } - debug(`ExposedThing '${this.title}' unsubscribes from event '${name}'`); + // if (this.events[name] != null) { + Helpers.validateInteractionOptions(this, this.events[name], options); + + const formIndex = ProtocolHelpers.getFormIndexForOperation( + this.events[name], + "event", + "unsubscribeevent", + options.formIndex + ); + if (formIndex !== -1) { + this.#eventListeners.unregister(this.events[name], formIndex, listener); } else { - throw new Error(`ExposedThing '${this.title}', no event found for '${name}'`); + throw new Error( + `ExposedThing '${this.title}', no event listener from found for '${name}' with form index '${options.formIndex}'` + ); + } + const unsubscribe = this.#eventHandlers.get(name)?.unsubscribe; + if (unsubscribe) { + unsubscribe(options); } + debug(`ExposedThing '${this.title}' unsubscribes from event '${name}'`); } /** @@ -612,30 +553,26 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { listener: ContentListener, options: WoT.InteractionOptions & { formIndex: number } ): Promise { - if (this.properties[name] != null) { - Helpers.validateInteractionOptions(this, this.properties[name], options); - const formIndex = ProtocolHelpers.getFormIndexForOperation( - this.properties[name], - "property", - "observeproperty", - options.formIndex + Helpers.validateInteractionOptions(this, this.properties[name], options); + const formIndex = ProtocolHelpers.getFormIndexForOperation( + this.properties[name], + "property", + "observeproperty", + options.formIndex + ); + + if (formIndex !== -1) { + this.#propertyListeners.register(this.properties[name], formIndex, listener); + debug(`ExposedThing '${this.title}' subscribes to property '${name}'`); + } else { + throw new Error( + `ExposedThing '${this.title}', no property listener from found for '${name}' with form index '${options.formIndex}'` ); + } - if (formIndex !== -1) { - this.#propertyListeners.register(this.properties[name], formIndex, listener); - debug(`ExposedThing '${this.title}' subscribes to property '${name}'`); - } else { - throw new Error( - `ExposedThing '${this.title}', no property listener from found for '${name}' with form index '${options.formIndex}'` - ); - } - - const observeHandler = this.#propertyHandlers.get(name)?.observeHandler; - if (observeHandler) { - await observeHandler(options); - } - } else { - throw new Error(`ExposedThing '${this.title}', no property found for '${name}'`); + const observeHandler = this.#propertyHandlers.get(name)?.observeHandler; + if (observeHandler) { + await observeHandler(options); } } @@ -644,29 +581,25 @@ export default class ExposedThing extends TD.Thing implements WoT.ExposedThing { listener: ContentListener, options: WoT.InteractionOptions & { formIndex: number } ): void { - if (this.properties[name] != null) { - Helpers.validateInteractionOptions(this, this.properties[name], options); - const formIndex = ProtocolHelpers.getFormIndexForOperation( - this.properties[name], - "property", - "unobserveproperty", - options.formIndex + Helpers.validateInteractionOptions(this, this.properties[name], options); + const formIndex = ProtocolHelpers.getFormIndexForOperation( + this.properties[name], + "property", + "unobserveproperty", + options.formIndex + ); + + if (formIndex !== -1) { + this.#propertyListeners.unregister(this.properties[name], formIndex, listener); + } else { + throw new Error( + `ExposedThing '${this.title}', no property listener from found for '${name}' with form index '${options.formIndex}'` ); + } - if (formIndex !== -1) { - this.#propertyListeners.unregister(this.properties[name], formIndex, listener); - } else { - throw new Error( - `ExposedThing '${this.title}', no property listener from found for '${name}' with form index '${options.formIndex}'` - ); - } - - const unobserveHandler = this.#propertyHandlers.get(name)?.unobserveHandler; - if (unobserveHandler) { - unobserveHandler(options); - } - } else { - throw new Error(`ExposedThing '${this.title}', no property found for '${name}'`); + const unobserveHandler = this.#propertyHandlers.get(name)?.unobserveHandler; + if (unobserveHandler) { + unobserveHandler(options); } } diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index e39fb8c48..38877d843 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -63,9 +63,6 @@ export default class Helpers implements Resolver { const parsed = new URL(uri); debug(parsed); // remove trailing ':' - if (parsed.protocol === null) { - throw new Error(`Protocol in url "${uri}" must be valid`); - } const scheme = parsed.protocol.slice(0, -1); debug(`Helpers found scheme '${scheme}'`); return scheme; @@ -243,7 +240,7 @@ export default class Helpers implements Resolver { } if (tdSchemaCopy.definitions != null) { - for (const [prop, propValue] of Object.entries(tdSchemaCopy.definitions) ?? []) { + for (const [prop, propValue] of Object.entries(tdSchemaCopy.definitions)) { tdSchemaCopy.definitions[prop] = this.createExposeThingInitSchema(propValue); } } @@ -351,12 +348,12 @@ export default class Helpers implements Resolver { uriVariables: { [k: string]: DataSchema } = {} ): Record { const params: Record = {}; - if (url == null || (uriVariables == null && globalUriVariables == null)) { + if (url == null) { return params; } - const queryparams = url.split("?")[1]; - if (queryparams == null) { + const queryparams = url.split("?")[1] as string | undefined; + if (queryparams === undefined) { return params; } const queries = queryparams.indexOf("&") !== -1 ? queryparams.split("&") : [queryparams]; @@ -367,14 +364,14 @@ export default class Helpers implements Resolver { const queryKey: string = decodeURIComponent(indexPair[0]); const queryValue: string = decodeURIComponent(indexPair.length > 1 ? indexPair[1] : ""); - if (uriVariables != null && uriVariables[queryKey] != null) { + if (Object.prototype.hasOwnProperty.call(uriVariables, queryKey)) { if (uriVariables[queryKey].type === "integer" || uriVariables[queryKey].type === "number") { // *cast* it to number params[queryKey] = +queryValue; } else { params[queryKey] = queryValue; } - } else if (globalUriVariables != null && globalUriVariables[queryKey] != null) { + } else if (Object.prototype.hasOwnProperty.call(globalUriVariables, queryKey)) { if (globalUriVariables[queryKey].type === "integer" || globalUriVariables[queryKey].type === "number") { // *cast* it to number params[queryKey] = +queryValue; diff --git a/packages/core/src/protocol-helpers.ts b/packages/core/src/protocol-helpers.ts index c2c5cdb77..b656d8407 100644 --- a/packages/core/src/protocol-helpers.ts +++ b/packages/core/src/protocol-helpers.ts @@ -19,7 +19,7 @@ import { ReadableStream as PolyfillStream } from "web-streams-polyfill"; import { ActionElement, EventElement, PropertyElement } from "wot-thing-description-types"; import { createLoggers } from "./logger"; -const { debug, warn } = createLoggers("core", "protocol-helpers"); +const { warn } = createLoggers("core", "protocol-helpers"); export interface IManagedStream { nodeStream: Readable; @@ -63,7 +63,7 @@ function isManaged(obj: unknown): obj is IManagedStream { export default class ProtocolHelpers { // set contentType (extend with more?) public static updatePropertyFormWithTemplate(form: Form, property: PropertyElement): void { - for (const formTemplate of property.forms ?? []) { + for (const formTemplate of property.forms) { // 1. Try to find match with correct href scheme if (formTemplate.href) { // TODO match for example http only? @@ -77,7 +77,7 @@ export default class ProtocolHelpers { } public static updateActionFormWithTemplate(form: Form, action: ActionElement): void { - for (const formTemplate of action.forms ?? []) { + for (const formTemplate of action.forms) { // 1. Try to find match with correct href scheme if (formTemplate.href) { // TODO match for example http only? @@ -91,7 +91,7 @@ export default class ProtocolHelpers { } public static updateEventFormWithTemplate(form: Form, event: EventElement): void { - for (const formTemplate of event.forms ?? []) { + for (const formTemplate of event.forms) { // 1. Try to find match with correct href scheme if (formTemplate.href) { // TODO match for example http only? @@ -112,16 +112,9 @@ export default class ProtocolHelpers { // try to find contentType (How to do this better) // Should interaction methods like readProperty() return an encapsulated value container with value&contentType // as sketched in https://github.com/w3c/wot-scripting-api/issues/201#issuecomment-573702999 - if ( - propertyName != null && - uriScheme != null && - td?.properties != null && - td.properties[propertyName] != null && - td.properties[propertyName].forms != null && - Array.isArray(td.properties[propertyName].forms) - ) { + if (td.properties != null && Array.isArray(td.properties[propertyName].forms)) { for (const form of td.properties[propertyName].forms) { - if (form.href?.startsWith(uriScheme) && form.contentType != null) { + if (form.href.startsWith(uriScheme) && form.contentType != null) { return form.contentType; // abort loop } } @@ -136,15 +129,9 @@ export default class ProtocolHelpers { uriScheme: string ): string | undefined { // try to find contentType - if ( - actionName != null && - uriScheme != null && - td?.actions && - td.actions != null && - Array.isArray(td.actions[actionName]?.forms) - ) { + if (td.actions && Array.isArray(td.actions[actionName].forms)) { for (const form of td.actions[actionName].forms) { - if (form.href && form.href.startsWith(uriScheme) && form.contentType != null) { + if (form.href.startsWith(uriScheme) && form.contentType != null) { return form.contentType; // abort loop } } @@ -159,13 +146,7 @@ export default class ProtocolHelpers { uriScheme: string ): string | undefined { // try to find contentType - if ( - eventName != null && - uriScheme != null && - td?.events && - td?.events[eventName]?.forms != null && - Array.isArray(td.events[eventName].forms) - ) { + if (td.events && Array.isArray(td.events[eventName].forms)) { for (const form of td.events[eventName].forms) { if (form.href && form.href.startsWith(uriScheme) && form.contentType != null) { return form.contentType; // abort loop @@ -235,26 +216,21 @@ export default class ProtocolHelpers { static readStreamFully(stream: NodeJS.ReadableStream): Promise> { return new Promise((resolve, reject) => { - if (stream != null) { - const chunks: Array = []; - stream.on("data", (data) => chunks.push(data)); - stream.on("error", reject); - stream.on("end", () => { - if ( - chunks[0] != null && - (chunks[0] instanceof Array || chunks[0] instanceof Buffer || chunks[0] instanceof Uint8Array) - ) { - resolve(Buffer.concat(chunks as Array)); - } else if (chunks[0] != null && typeof chunks[0] === "string") { - resolve(Buffer.from(chunks.join())); - } else { - resolve(Buffer.from(chunks as Array)); - } - }); - } else { - debug(`Protocol-Helper returns empty buffer for readStreamFully due to undefined stream`); - resolve(Buffer.alloc(0)); - } + const chunks: Array = []; + stream.on("data", (data) => chunks.push(data)); + stream.on("error", reject); + stream.on("end", () => { + if ( + chunks[0] != null && + (chunks[0] instanceof Array || chunks[0] instanceof Buffer || chunks[0] instanceof Uint8Array) + ) { + resolve(Buffer.concat(chunks as Array)); + } else if (chunks[0] != null && typeof chunks[0] === "string") { + resolve(Buffer.from(chunks.join())); + } else { + resolve(Buffer.from(chunks as Array)); + } + }); }); } @@ -345,15 +321,15 @@ export default class ProtocolHelpers { } // If a form index hint is gived, you it. Just check the form actually supports the op - if (interaction.forms !== undefined && formIndex !== undefined && interaction.forms.length > formIndex) { + if (formIndex !== undefined && interaction.forms.length > formIndex) { const form = interaction.forms[formIndex]; - if (form != null && (operationName == null || form.op?.includes(operationName) === true)) { + if (operationName == null || form.op?.includes(operationName) === true) { finalFormIndex = formIndex; } } // If no form was found yet, loop through all forms - if (interaction.forms !== undefined && finalFormIndex === -1) { + if (finalFormIndex === -1) { if (operationName !== undefined) { interaction.forms.every((form: Form) => { // operationName !== undefined @@ -363,12 +339,9 @@ export default class ProtocolHelpers { } return finalFormIndex === -1; }); - } else { - interaction.forms.every((form: Form) => { - // interaction.forms !== undefined - finalFormIndex = interaction.forms!.indexOf(form); - return false; - }); + } else if (interaction.forms.length > 0) { + // interaction.forms !== undefined + finalFormIndex = 0; } } diff --git a/packages/core/src/protocol-listener-registry.ts b/packages/core/src/protocol-listener-registry.ts index b5575488f..3285bc01a 100644 --- a/packages/core/src/protocol-listener-registry.ts +++ b/packages/core/src/protocol-listener-registry.ts @@ -21,11 +21,6 @@ export default class ProtocolListenerRegistry { private static EMPTY_MAP = new Map(); private listeners: Map> = new Map(); register(affordance: ThingInteraction, formIndex: number, listener: ContentListener): void { - if (affordance.forms[formIndex] == null) { - throw new Error( - "Can't register the listener for affordance with formIndex. The affordance does not contain the form" - ); - } let formMap = this.listeners.get(affordance); diff --git a/packages/core/src/serdes.ts b/packages/core/src/serdes.ts index 81dabb642..e3d63311d 100644 --- a/packages/core/src/serdes.ts +++ b/packages/core/src/serdes.ts @@ -27,7 +27,7 @@ import { ThingDescription, } from "wot-thing-description-types"; -const { debug, warn } = createLoggers("core", "serdes"); +const { debug } = createLoggers("core", "serdes"); type AffordanceElement = PropertyElement | ActionElement | EventElement; @@ -91,9 +91,8 @@ export function parseTD(td: string, normalize?: boolean): Thing { // apply defaults as per WoT Thing Description spec - if (thing["@context"] === undefined) { - thing["@context"] = [TD.DEFAULT_CONTEXT_V1, TD.DEFAULT_CONTEXT_V11]; - } else if (Array.isArray(thing["@context"])) { + thing["@context"] = [TD.DEFAULT_CONTEXT_V1, TD.DEFAULT_CONTEXT_V11]; + if (Array.isArray(thing["@context"])) { let semContext = thing["@context"] as Array; const indexV1 = semContext.indexOf(TD.DEFAULT_CONTEXT_V1); const indexV11 = semContext.indexOf(TD.DEFAULT_CONTEXT_V11); @@ -168,9 +167,6 @@ export function parseTD(td: string, normalize?: boolean): Thing { adjustAffordanceField(thing, affordanceKey); } - if (thing.security === undefined) { - warn("parseTD() found no security metadata"); - } // wrap in array for later simplification if (typeof thing.security === "string") { thing.security = [thing.security]; @@ -181,9 +177,6 @@ export function parseTD(td: string, normalize?: boolean): Thing { // properties for (const [propName, prop] of Object.entries(thing.properties ?? {})) { // ensure forms mandatory forms field - if (prop.forms == null) { - throw new Error(`Property '${propName}' has no forms field`); - } for (const form of prop.forms) { if (!form.href) { throw new Error(`Form of Property '${propName}' has no href field`); @@ -198,9 +191,6 @@ export function parseTD(td: string, normalize?: boolean): Thing { // actions for (const [actName, act] of Object.entries(thing.actions ?? {})) { // ensure forms mandatory forms field - if (act.forms == null) { - throw new Error(`Action '${actName}' has no forms field`); - } for (const form of act.forms) { if (!form.href) { throw new Error(`Form of Action '${actName}' has no href field`); @@ -215,9 +205,6 @@ export function parseTD(td: string, normalize?: boolean): Thing { // events for (const [evtName, evt] of Object.entries(thing.events ?? {})) { // ensure forms mandatory forms field - if (evt.forms == null) { - throw new Error(`Event '${evtName}' has no forms field`); - } for (const form of evt.forms) { if (!form.href) { throw new Error(`Form of Event '${evtName}' has no href field`); @@ -251,7 +238,7 @@ export function serializeTD(thing: Thing): string { const copy: Thing = JSON.parse(JSON.stringify(thing)); // clean-ups - if (copy.security == null || copy.security.length === 0) { + if (copy.security.length === 0) { copy.securityDefinitions = { nosec_sc: { scheme: "nosec" }, }; @@ -287,7 +274,7 @@ export function serializeTD(thing: Thing): string { delete copy.events; } - if (copy?.links.length === 0) { + if (copy.links?.length === 0) { delete copy.links; } diff --git a/packages/core/src/servient.ts b/packages/core/src/servient.ts index 99261b4d2..f7c3cd824 100644 --- a/packages/core/src/servient.ts +++ b/packages/core/src/servient.ts @@ -178,7 +178,7 @@ export default class Servient { } public addCredentials(credentials: Record): void { - for (const [credentialKey, credentialValue] of Object.entries(credentials ?? {})) { + for (const [credentialKey, credentialValue] of Object.entries(credentials)) { debug(`Servient storing credentials for '${credentialKey}'`); const currentCredentials = this.credentialStore.get(credentialKey) ?? []; if (currentCredentials.length === 0) { diff --git a/packages/core/test/content-serdes-test.ts b/packages/core/test/content-serdes-test.ts index d67b460a7..160d0fcca 100644 --- a/packages/core/test/content-serdes-test.ts +++ b/packages/core/test/content-serdes-test.ts @@ -71,7 +71,7 @@ const checkStreamToValue = ( expect( ContentSerdes.contentToValue( { type: contentType, body: octectBuffer }, - { type: type ?? "integer", properties: {}, ...schema } + { type: type, properties: {}, ...schema } ) ).to.deep.equal(match); }; diff --git a/packages/core/test/server-test.ts b/packages/core/test/server-test.ts index 8216aa370..c6ba6d0f5 100644 --- a/packages/core/test/server-test.ts +++ b/packages/core/test/server-test.ts @@ -273,7 +273,7 @@ class WoTServerTest { expect(thing).to.have.property("properties").to.have.property("my number"); // Check internals, how to to check handlers properly with *some* type-safety - const ff = await readHandler?.(); + const ff = await readHandler(); expect(ff).to.equal(1); } From 8076ad751783625ebac28a9d20c75128979d11b1 Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 15 Feb 2026 18:44:06 +0530 Subject: [PATCH 02/10] fix(core) : fixed the formating issue --- packages/core/src/codecs/octetstream-codec.ts | 30 +++++++++---------- packages/core/src/consumed-thing.ts | 3 +- packages/core/src/content-serdes.ts | 14 ++++----- .../core/src/protocol-listener-registry.ts | 1 - 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/core/src/codecs/octetstream-codec.ts b/packages/core/src/codecs/octetstream-codec.ts index 86ffadaaf..4823e52d7 100644 --- a/packages/core/src/codecs/octetstream-codec.ts +++ b/packages/core/src/codecs/octetstream-codec.ts @@ -183,8 +183,8 @@ export default class OctetstreamCodec implements ContentCodec { ? bytes.readInt16BE(0) : bytes.readUInt16BE(0) : signed - ? bytes.readInt16LE(0) - : bytes.readUInt16LE(0); + ? bytes.readInt16LE(0) + : bytes.readUInt16LE(0); case 32: return bigEndian @@ -192,8 +192,8 @@ export default class OctetstreamCodec implements ContentCodec { ? bytes.readInt32BE(0) : bytes.readUInt32BE(0) : signed - ? bytes.readInt32LE(0) - : bytes.readUInt32LE(0); + ? bytes.readInt32LE(0) + : bytes.readUInt32LE(0); default: { const result = bigEndian @@ -201,8 +201,8 @@ export default class OctetstreamCodec implements ContentCodec { ? bytes.readIntBE(0, dataLength / 8) : bytes.readUIntBE(0, dataLength / 8) : signed - ? bytes.readIntLE(0, dataLength / 8) - : bytes.readUIntLE(0, dataLength / 8); + ? bytes.readIntLE(0, dataLength / 8) + : bytes.readUIntLE(0, dataLength / 8); // warn about numbers being too big to be represented as safe integers if (!Number.isSafeInteger(result)) { warn("Result is not a safe integer"); @@ -311,7 +311,7 @@ export default class OctetstreamCodec implements ContentCodec { if (parseInt(typeSem[3]) !== bitLength) { throw new Error( `Type is '${(typeSem[1] || "") + typeSem[2] + typeSem[3]}' but 'ex:bitLength' is ` + - bitLength + bitLength ); } } else { @@ -430,10 +430,10 @@ export default class OctetstreamCodec implements ContentCodec { if (value < 0 || value > limit) { throw new Error( "Integer overflow when representing " + - value + - " as an unsigned integer using " + - length + - " bit(s)" + value + + " as an unsigned integer using " + + length + + " bit(s)" ); } } @@ -460,8 +460,8 @@ export default class OctetstreamCodec implements ContentCodec { ? buf.writeInt16BE(value, 0) : buf.writeUInt16BE(value, 0) : signed - ? buf.writeInt16LE(value, 0) - : buf.writeUInt16LE(value, 0); + ? buf.writeInt16LE(value, 0) + : buf.writeUInt16LE(value, 0); break; case 4: @@ -470,8 +470,8 @@ export default class OctetstreamCodec implements ContentCodec { ? buf.writeInt32BE(value, 0) : buf.writeUInt32BE(value, 0) : signed - ? buf.writeInt32LE(value, 0) - : buf.writeUInt32LE(value, 0); + ? buf.writeInt32LE(value, 0) + : buf.writeUInt32LE(value, 0); break; default: diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index b9007ee61..34d27e3bd 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -704,7 +704,8 @@ export default class ConsumedThing extends Thing implements IConsumedThing { throw new Error(`ConsumedThing '${this.title}' did not get suitable form`); } debug( - `ConsumedThing '${this.title}' invoking ${form.href}${parameter !== undefined ? " with '" + parameter + "'" : "" + `ConsumedThing '${this.title}' invoking ${form.href}${ + parameter !== undefined ? " with '" + parameter + "'" : "" }` ); diff --git a/packages/core/src/content-serdes.ts b/packages/core/src/content-serdes.ts index c16800928..1a269d026 100644 --- a/packages/core/src/content-serdes.ts +++ b/packages/core/src/content-serdes.ts @@ -125,13 +125,13 @@ export class ContentSerdes { } public contentToValue(content: ReadContent, schema: DataSchema): DataSchemaValue | undefined { - if (content.body.byteLength > 0) { - // default to application/json - content.type = ContentSerdes.DEFAULT; - } else { - // empty payload without media type -> void/undefined (note: e.g., empty payload with text/plain -> "") - return undefined; - } + if (content.body.byteLength > 0) { + // default to application/json + content.type = ContentSerdes.DEFAULT; + } else { + // empty payload without media type -> void/undefined (note: e.g., empty payload with text/plain -> "") + return undefined; + } // split into media type and parameters const mt = ContentSerdes.getMediaType(content.type); diff --git a/packages/core/src/protocol-listener-registry.ts b/packages/core/src/protocol-listener-registry.ts index 3285bc01a..6164f1ffe 100644 --- a/packages/core/src/protocol-listener-registry.ts +++ b/packages/core/src/protocol-listener-registry.ts @@ -21,7 +21,6 @@ export default class ProtocolListenerRegistry { private static EMPTY_MAP = new Map(); private listeners: Map> = new Map(); register(affordance: ThingInteraction, formIndex: number, listener: ContentListener): void { - let formMap = this.listeners.get(affordance); if (!formMap) { From 6a44aea41a24408a1e5dc7e200b4c73e68825d1a Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 15 Feb 2026 19:03:26 +0530 Subject: [PATCH 03/10] fix(core): add mjs files to format script and apply consistent quoting in eslint config. --- packages/core/eslint.config.mjs | 14 +++++++------- packages/core/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/eslint.config.mjs b/packages/core/eslint.config.mjs index cffb236c9..ff186ee27 100644 --- a/packages/core/eslint.config.mjs +++ b/packages/core/eslint.config.mjs @@ -1,10 +1,10 @@ -import rootConfig from '../../eslint.config.mjs'; +import rootConfig from "../../eslint.config.mjs"; export default [ - ...rootConfig, - { - rules: { - '@typescript-eslint/no-unnecessary-condition': 'warn' - } - } + ...rootConfig, + { + rules: { + " @typescript-eslint/no-unnecessary-condition": "warn", + }, + }, ]; diff --git a/packages/core/package.json b/packages/core/package.json index 76b0a0b7f..4aa5a670b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -40,7 +40,7 @@ "build": "tsc -b", "lint": "eslint .", "lint:fix": "eslint . --fix", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"**/*.json\"", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"**/*.json\" \"*.mjs\"", "test": "mocha --require ts-node/register --extension ts" }, "bugs": { From 81dd1c39e0f8011a2c4ce909f59919b67b9d888b Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 15 Feb 2026 19:07:41 +0530 Subject: [PATCH 04/10] chore: Promote `@typescript-eslint/no-unnecessary-condition` ESLint rule from warn to error. --- packages/core/eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/eslint.config.mjs b/packages/core/eslint.config.mjs index ff186ee27..265a99010 100644 --- a/packages/core/eslint.config.mjs +++ b/packages/core/eslint.config.mjs @@ -4,7 +4,7 @@ export default [ ...rootConfig, { rules: { - " @typescript-eslint/no-unnecessary-condition": "warn", + "@typescript-eslint/no-unnecessary-condition": "error", }, }, ]; From fd5a1e8f838abbf2e7b68b40ccda2614a0817a49 Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 15 Feb 2026 19:15:11 +0530 Subject: [PATCH 05/10] revert: content-serdes --- packages/core/src/content-serdes.ts | 45 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/core/src/content-serdes.ts b/packages/core/src/content-serdes.ts index 1a269d026..19d9557c0 100644 --- a/packages/core/src/content-serdes.ts +++ b/packages/core/src/content-serdes.ts @@ -57,28 +57,29 @@ export class ContentSerdes { private offered: Set = new Set(); public static get(): ContentSerdes { - this.instance = new ContentSerdes(); - // JSON - this.instance.addCodec(new JsonCodec(), true); - this.instance.addCodec(new JsonCodec("application/senml+json")); - this.instance.addCodec(new JsonCodec("application/td+json")); - this.instance.addCodec(new JsonCodec("application/ld+json")); - // CBOR - this.instance.addCodec(new CborCodec(), true); - // Text - this.instance.addCodec(new TextCodec()); - this.instance.addCodec(new TextCodec("text/html")); - this.instance.addCodec(new TextCodec("text/css")); - this.instance.addCodec(new TextCodec("application/xml")); - this.instance.addCodec(new TextCodec("application/xhtml+xml")); - this.instance.addCodec(new TextCodec("image/svg+xml")); - // Base64 - this.instance.addCodec(new Base64Codec("image/png")); - this.instance.addCodec(new Base64Codec("image/gif")); - this.instance.addCodec(new Base64Codec("image/jpeg")); - // OctetStream - this.instance.addCodec(new OctetstreamCodec()); - + if (!this.instance) { + this.instance = new ContentSerdes(); + // JSON + this.instance.addCodec(new JsonCodec(), true); + this.instance.addCodec(new JsonCodec("application/senml+json")); + this.instance.addCodec(new JsonCodec("application/td+json")); + this.instance.addCodec(new JsonCodec("application/ld+json")); + // CBOR + this.instance.addCodec(new CborCodec(), true); + // Text + this.instance.addCodec(new TextCodec()); + this.instance.addCodec(new TextCodec("text/html")); + this.instance.addCodec(new TextCodec("text/css")); + this.instance.addCodec(new TextCodec("application/xml")); + this.instance.addCodec(new TextCodec("application/xhtml+xml")); + this.instance.addCodec(new TextCodec("image/svg+xml")); + // Base64 + this.instance.addCodec(new Base64Codec("image/png")); + this.instance.addCodec(new Base64Codec("image/gif")); + this.instance.addCodec(new Base64Codec("image/jpeg")); + // OctetStream + this.instance.addCodec(new OctetstreamCodec()); + } return this.instance; } From a6f5dba7c98fee63ab47bc3323ad8bb63cbd2d32 Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 15 Feb 2026 19:39:18 +0530 Subject: [PATCH 06/10] fix(core) : lint error --- packages/core/src/content-serdes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/content-serdes.ts b/packages/core/src/content-serdes.ts index 19d9557c0..ee4d20055 100644 --- a/packages/core/src/content-serdes.ts +++ b/packages/core/src/content-serdes.ts @@ -47,7 +47,7 @@ interface ReadContent { * it can accept multiple serializers and decoders */ export class ContentSerdes { - private static instance: ContentSerdes; + private static instance: ContentSerdes | undefined; public static readonly DEFAULT: string = "application/json"; public static readonly TD: string = "application/td+json"; From 8de8c6798116f88f71581bcc055e6e3256a3b067 Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 22 Feb 2026 04:21:37 +0530 Subject: [PATCH 07/10] fix: added explicit null check to octet stream codec value validation. --- packages/core/src/codecs/octetstream-codec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/codecs/octetstream-codec.ts b/packages/core/src/codecs/octetstream-codec.ts index 4823e52d7..fff6f20b9 100644 --- a/packages/core/src/codecs/octetstream-codec.ts +++ b/packages/core/src/codecs/octetstream-codec.ts @@ -584,7 +584,8 @@ export default class OctetstreamCodec implements ContentCodec { parameters: { [key: string]: string | undefined } = {}, result?: Buffer | undefined ): Buffer { - if (typeof value !== "object") { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (typeof value !== "object" || value === null) { throw new Error("Value is not an object"); } From a96a28040754bb3f9aad52bbee52c94356ae1cc5 Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 22 Feb 2026 17:05:02 +0530 Subject: [PATCH 08/10] fix: reverted validation check --- packages/core/src/consumed-thing.ts | 8 ++++++++ packages/core/src/protocol-listener-registry.ts | 4 ++++ packages/core/src/serdes.ts | 12 ++++++++++++ 3 files changed, 24 insertions(+) diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index 34d27e3bd..6cdfb2cba 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -130,6 +130,10 @@ class InternalPropertySubscription extends InternalSubscription { private readonly form: FormElementProperty ) { super(thing, name, client); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!this.thing.properties[name].forms) { + throw new Error(`Property '${name}' has no forms`); + } const index = this.thing.properties[name].forms.indexOf(form as Form); if (index < 0) { throw new Error(`Could not find form ${form.href} in property ${name}`); @@ -258,6 +262,10 @@ class InternalEventSubscription extends InternalSubscription { private readonly form: FormElementEvent ) { super(thing, name, client); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!this.thing.events[name].forms) { + throw new Error(`Event '${name}' has no forms`); + } const index = this.thing.events[name].forms.indexOf(form as Form); if (index < 0) { throw new Error(`Could not find form ${form.href} in event ${name}`); diff --git a/packages/core/src/protocol-listener-registry.ts b/packages/core/src/protocol-listener-registry.ts index 6164f1ffe..109784669 100644 --- a/packages/core/src/protocol-listener-registry.ts +++ b/packages/core/src/protocol-listener-registry.ts @@ -70,6 +70,10 @@ export default class ProtocolListenerRegistry { if (formIndex !== undefined) { const listeners = formMap.get(formIndex); if (listeners) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!affordance.forms || affordance.forms[formIndex] === undefined) { + throw new Error(`Form at index ${formIndex} does not exist`); + } const contentType = affordance.forms[formIndex].contentType; const content = contentSerdes.valueToContent(data, schema, contentType); diff --git a/packages/core/src/serdes.ts b/packages/core/src/serdes.ts index e3d63311d..a28928b7a 100644 --- a/packages/core/src/serdes.ts +++ b/packages/core/src/serdes.ts @@ -176,6 +176,10 @@ export function parseTD(td: string, normalize?: boolean): Thing { const allForms = []; // properties for (const [propName, prop] of Object.entries(thing.properties ?? {})) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!prop.forms || !Array.isArray(prop.forms)) { + throw new Error(`Property '${propName}' has no forms field`); + } // ensure forms mandatory forms field for (const form of prop.forms) { if (!form.href) { @@ -190,6 +194,10 @@ export function parseTD(td: string, normalize?: boolean): Thing { } // actions for (const [actName, act] of Object.entries(thing.actions ?? {})) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!act.forms || !Array.isArray(act.forms)) { + throw new Error(`Action '${actName}' has no forms field`); + } // ensure forms mandatory forms field for (const form of act.forms) { if (!form.href) { @@ -204,6 +212,10 @@ export function parseTD(td: string, normalize?: boolean): Thing { } // events for (const [evtName, evt] of Object.entries(thing.events ?? {})) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!evt.forms || !Array.isArray(evt.forms)) { + throw new Error(`Event '${evtName}' has no forms field`); + } // ensure forms mandatory forms field for (const form of evt.forms) { if (!form.href) { From d7fac9e82b86d92f4aa590ed32b33c54575bb6a4 Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 22 Feb 2026 18:17:32 +0530 Subject: [PATCH 09/10] fix: adding null/undefined checks and refining default value assignments --- packages/core/src/consumed-thing.ts | 9 ++++++--- packages/core/src/content-serdes.ts | 20 +++++++++++-------- packages/core/src/protocol-helpers.ts | 6 ++++++ .../core/src/protocol-listener-registry.ts | 7 +++++++ packages/core/src/serdes.ts | 7 +++++-- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/core/src/consumed-thing.ts b/packages/core/src/consumed-thing.ts index 6cdfb2cba..93bafc747 100644 --- a/packages/core/src/consumed-thing.ts +++ b/packages/core/src/consumed-thing.ts @@ -463,7 +463,8 @@ export default class ConsumedThing extends Thing implements IConsumedThing { let ws: SecurityScheme | undefined = this.securityDefinitions[s]; // also push nosec in case of proxy - if (ws.scheme === "combo") { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (ws?.scheme === "combo") { ws = resolveComboScheme(ws as ComboSecurityScheme, s); } if (ws != null) { @@ -592,7 +593,8 @@ export default class ConsumedThing extends Thing implements IConsumedThing { outputDataSchema: WoT.DataSchema | undefined ): InteractionOutput { // infer media type from form if not in response metadata - content.type = form.contentType ?? "application/json"; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + content.type ??= form.contentType ?? "application/json"; // check if returned media type is the same as expected media type (from TD) this.checkMediaTypeOrThrow(content, form); return new InteractionOutput(content, form, outputDataSchema); @@ -618,7 +620,8 @@ export default class ConsumedThing extends Thing implements IConsumedThing { synchronous?: boolean ): ActionInteractionOutput { // infer media type from form if not in response metadata - content.type = form.contentType ?? "application/json"; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + content.type ??= form.contentType ?? "application/json"; // check if returned media type is the same as expected media type (from TD) this.checkMediaTypeOrThrow(content, form); return new ActionInteractionOutput(content, form, outputDataSchema, synchronous); diff --git a/packages/core/src/content-serdes.ts b/packages/core/src/content-serdes.ts index ee4d20055..245bb8c03 100644 --- a/packages/core/src/content-serdes.ts +++ b/packages/core/src/content-serdes.ts @@ -47,7 +47,7 @@ interface ReadContent { * it can accept multiple serializers and decoders */ export class ContentSerdes { - private static instance: ContentSerdes | undefined; + private static instance: ContentSerdes; public static readonly DEFAULT: string = "application/json"; public static readonly TD: string = "application/td+json"; @@ -57,7 +57,8 @@ export class ContentSerdes { private offered: Set = new Set(); public static get(): ContentSerdes { - if (!this.instance) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.instance == null) { this.instance = new ContentSerdes(); // JSON this.instance.addCodec(new JsonCodec(), true); @@ -126,12 +127,15 @@ export class ContentSerdes { } public contentToValue(content: ReadContent, schema: DataSchema): DataSchemaValue | undefined { - if (content.body.byteLength > 0) { - // default to application/json - content.type = ContentSerdes.DEFAULT; - } else { - // empty payload without media type -> void/undefined (note: e.g., empty payload with text/plain -> "") - return undefined; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (content.type === undefined) { + if (content.body.byteLength > 0) { + // default to application/json + content.type = ContentSerdes.DEFAULT; + } else { + // empty payload without media type -> void/undefined (note: e.g., empty payload with text/plain -> "") + return undefined; + } } // split into media type and parameters diff --git a/packages/core/src/protocol-helpers.ts b/packages/core/src/protocol-helpers.ts index b656d8407..47199637c 100644 --- a/packages/core/src/protocol-helpers.ts +++ b/packages/core/src/protocol-helpers.ts @@ -63,6 +63,8 @@ function isManaged(obj: unknown): obj is IManagedStream { export default class ProtocolHelpers { // set contentType (extend with more?) public static updatePropertyFormWithTemplate(form: Form, property: PropertyElement): void { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (property.forms == null) return; for (const formTemplate of property.forms) { // 1. Try to find match with correct href scheme if (formTemplate.href) { @@ -77,6 +79,8 @@ export default class ProtocolHelpers { } public static updateActionFormWithTemplate(form: Form, action: ActionElement): void { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (action.forms == null) return; for (const formTemplate of action.forms) { // 1. Try to find match with correct href scheme if (formTemplate.href) { @@ -91,6 +95,8 @@ export default class ProtocolHelpers { } public static updateEventFormWithTemplate(form: Form, event: EventElement): void { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (event.forms == null) return; for (const formTemplate of event.forms) { // 1. Try to find match with correct href scheme if (formTemplate.href) { diff --git a/packages/core/src/protocol-listener-registry.ts b/packages/core/src/protocol-listener-registry.ts index 109784669..6a6abdddc 100644 --- a/packages/core/src/protocol-listener-registry.ts +++ b/packages/core/src/protocol-listener-registry.ts @@ -21,6 +21,13 @@ export default class ProtocolListenerRegistry { private static EMPTY_MAP = new Map(); private listeners: Map> = new Map(); register(affordance: ThingInteraction, formIndex: number, listener: ContentListener): void { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (affordance.forms[formIndex] == null) { + throw new Error( + "Can't register the listener for affordance with formIndex. The affordance does not contain the form" + ); + } + let formMap = this.listeners.get(affordance); if (!formMap) { diff --git a/packages/core/src/serdes.ts b/packages/core/src/serdes.ts index a28928b7a..022e4341b 100644 --- a/packages/core/src/serdes.ts +++ b/packages/core/src/serdes.ts @@ -91,8 +91,10 @@ export function parseTD(td: string, normalize?: boolean): Thing { // apply defaults as per WoT Thing Description spec - thing["@context"] = [TD.DEFAULT_CONTEXT_V1, TD.DEFAULT_CONTEXT_V11]; - if (Array.isArray(thing["@context"])) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (thing["@context"] === undefined) { + thing["@context"] = [TD.DEFAULT_CONTEXT_V1, TD.DEFAULT_CONTEXT_V11]; + } else if (Array.isArray(thing["@context"])) { let semContext = thing["@context"] as Array; const indexV1 = semContext.indexOf(TD.DEFAULT_CONTEXT_V1); const indexV11 = semContext.indexOf(TD.DEFAULT_CONTEXT_V11); @@ -130,6 +132,7 @@ export function parseTD(td: string, normalize?: boolean): Thing { } thing["@context"] = semContext as ThingContext; } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (thing["@context"] !== TD.DEFAULT_CONTEXT_V1 && thing["@context"] !== TD.DEFAULT_CONTEXT_V11) { const semContext = thing["@context"]; // insert default contexts as first entries From 909dd3b870cc545e8dbd3538e243e55a5ca66bf8 Mon Sep 17 00:00:00 2001 From: ViRUS-0-0 Date: Sun, 22 Feb 2026 18:26:40 +0530 Subject: [PATCH 10/10] fix: guard addCredentials against undefined input --- packages/core/src/servient.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/servient.ts b/packages/core/src/servient.ts index f7c3cd824..4b52ff868 100644 --- a/packages/core/src/servient.ts +++ b/packages/core/src/servient.ts @@ -178,7 +178,8 @@ export default class Servient { } public addCredentials(credentials: Record): void { - for (const [credentialKey, credentialValue] of Object.entries(credentials)) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + for (const [credentialKey, credentialValue] of Object.entries(credentials ?? {})) { debug(`Servient storing credentials for '${credentialKey}'`); const currentCredentials = this.credentialStore.get(credentialKey) ?? []; if (currentCredentials.length === 0) {