Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Executes Javascript, Typescript Scripts.
### **WORK IN PROGRESS**
* (arteck) Performance optimizations part 2
* (arteck) fix filter in tab scripts
* (@GermanBluefox) Fixed a subscription leak on script stop for RegExp-notation string ids (dispatch index)
* (@GermanBluefox) `extendObject` no longer throws into the script when the object contains non-clonable values (e.g. functions)

### 9.2.3 (2026-05-27)
* (arteck) Performance optimizations done
Expand Down
94 changes: 59 additions & 35 deletions build/lib/sandbox.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion build/lib/sandbox.js.map

Large diffs are not rendered by default.

25 changes: 4 additions & 21 deletions build/main.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion build/main.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src-admin/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const config = {
'./Components': './src/Components.tsx',
},
remotes: {},
dts: false,
shared: moduleFederationShared(JSON.parse(readFileSync('./package.json').toString())),
}),
// react(),
Expand Down
96 changes: 58 additions & 38 deletions src/lib/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,39 @@ const _selectorRegExpCache = new Map<string, RegExp>();
/** Monotonically increasing handler-ID counter – avoids Date.now()+random collisions */
let _handlerIdCounter = 1;

/** Returns true when patId is a plain exact state-ID (no wildcards, no RegExp notation). */
export function isExactId(patId: unknown): patId is string {
return (
!!patId && typeof patId === 'string' && !patId.includes('*') && !patId.includes('?') && !patId.startsWith('/')
);
}

/**
* Removes a subscription from the O(1) dispatch index (subscriptionsMap / subscriptionsWildcard).
* Must use the same exact-id classification as the subscribe side (isExactId),
* otherwise entries leak in the structure that was not searched.
*/
export function removeFromDispatchIndex(ctx: JavascriptContext, sub: SubscriptionResult): void {
const patId = sub.pattern?.id;
if (isExactId(patId)) {
const bucket = ctx.subscriptionsMap.get(patId);
if (bucket) {
const pos = bucket.indexOf(sub);
if (pos !== -1) {
bucket.splice(pos, 1);
}
if (bucket.length === 0) {
ctx.subscriptionsMap.delete(patId);
}
}
} else {
const wPos = ctx.subscriptionsWildcard.indexOf(sub);
if (wPos !== -1) {
ctx.subscriptionsWildcard.splice(wPos, 1);
}
}
}

export function sandBox(
script: JsScript,
name: string,
Expand Down Expand Up @@ -193,39 +226,6 @@ export function sandBox(
}
}

/** Returns true when patId is a plain exact state-ID (no wildcards, no RegExp notation). */
function _isExactId(patId: unknown): patId is string {
return (
!!patId &&
typeof patId === 'string' &&
!patId.includes('*') &&
!patId.includes('?') &&
!patId.startsWith('/')
);
}

/** Removes a subscription from the O(1) dispatch index (subscriptionsMap / subscriptionsWildcard). */
function _removeFromDispatchIndex(ctx: JavascriptContext, sub: SubscriptionResult): void {
const patId = sub.pattern?.id;
if (_isExactId(patId)) {
const bucket = ctx.subscriptionsMap.get(patId);
if (bucket) {
const pos = bucket.indexOf(sub);
if (pos !== -1) {
bucket.splice(pos, 1);
}
if (bucket.length === 0) {
ctx.subscriptionsMap.delete(patId);
}
}
} else {
const wPos = ctx.subscriptionsWildcard.indexOf(sub);
if (wPos !== -1) {
ctx.subscriptionsWildcard.splice(wPos, 1);
}
}
}

function getPatternCompareFunctions(pattern: Pattern): PatternEventCompareFunction[] & { logic?: 'and' | 'or' } {
let func: PatternEventCompareFunction;
const functions: PatternEventCompareFunction[] & { logic?: 'and' | 'or' } = [];
Expand Down Expand Up @@ -1881,7 +1881,7 @@ export function sandBox(
context.subscriptions.push(subs);

// O(1) dispatch index: exact string IDs go into the map, everything else into the wildcard array
if (_isExactId(oPattern.id)) {
if (isExactId(oPattern.id)) {
if (!context.subscriptionsMap.has(oPattern.id)) {
context.subscriptionsMap.set(oPattern.id, []);
}
Expand Down Expand Up @@ -1972,7 +1972,7 @@ export function sandBox(
unsubscribePattern(script, sub.pattern.id as string);
context.subscriptions.splice(i, 1);
// Remove from O(1) dispatch structures
_removeFromDispatchIndex(context, sub);
removeFromDispatchIndex(context, sub);
sandbox.__engine.__subscriptions--;
return true;
}
Expand All @@ -1987,7 +1987,7 @@ export function sandBox(
unsubscribePattern(script, sub.pattern.id as string);
context.subscriptions.splice(i, 1);
// Remove from O(1) dispatch structures
_removeFromDispatchIndex(context, sub);
removeFromDispatchIndex(context, sub);
sandbox.__engine.__subscriptions--;
}
}
Expand Down Expand Up @@ -5854,10 +5854,30 @@ export function sandBox(
if (sandbox.verbose) {
sandbox.log(`extendObject(id=${id}, obj=${JSON.stringify(obj)})`, 'info');
}
let objClone: Partial<ioBroker.Object>;
try {
objClone = structuredClone(obj);
} catch (err: unknown) {
// e.g. DataCloneError when the object contains functions
void adapter.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, {
val: true,
ack: true,
c: 'extendObject',
});
sandbox.log(`Object "${id}" can't be copied: ${JSON.stringify(err)}`, 'error');
if (typeof callback === 'function') {
try {
callback.call(sandbox, new Error(`Object "${id}" can't be copied`));
} catch (err: unknown) {
errorInCallback(err as Error);
}
}
return;
}
if (callback) {
adapter.extendForeignObject(id, structuredClone(obj), callback);
adapter.extendForeignObject(id, objClone, callback);
} else {
void adapter.extendForeignObject(id, structuredClone(obj));
void adapter.extendForeignObject(id, objClone);
}
}
};
Expand Down
28 changes: 5 additions & 23 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import type { CompileResult } from 'virtual-tsc/build/util';
import { Mirror } from './lib/mirror';
import ProtectFs from './lib/protectFs';
import { setLanguage, getLanguage } from './lib/words';
import { sandBox } from './lib/sandbox';
import { sandBox, removeFromDispatchIndex } from './lib/sandbox';
import { requestModuleNameByUrl } from './lib/nodeModulesManagement';
import { resolveProviderCredentials, resolveTestCredentials, listAvailableProviders } from './lib/aiProviderResolver';
import {
Expand Down Expand Up @@ -2828,28 +2828,10 @@ class JavaScript extends Adapter {
for (let i = this.subscriptions.length - 1; i >= 0; i--) {
if (this.subscriptions[i].name === name) {
const sub = this.subscriptions.splice(i, 1)[0];
// Also remove from O(1) dispatch structures
if (
sub?.pattern.id &&
typeof sub.pattern.id === 'string' &&
!sub.pattern.id.includes('*') &&
!sub.pattern.id.includes('?')
) {
const bucket = this.subscriptionsMap.get(sub.pattern.id);
if (bucket) {
const pos = bucket.indexOf(sub);
if (pos !== -1) {
bucket.splice(pos, 1);
}
if (bucket.length === 0) {
this.subscriptionsMap.delete(sub.pattern.id);
}
}
} else {
const wPos = this.subscriptionsWildcard.indexOf(sub);
if (wPos !== -1) {
this.subscriptionsWildcard.splice(wPos, 1);
}
// Also remove from the O(1) dispatch structures – shared helper to keep the
// exact-id classification identical to the subscribe side in sandbox.ts
if (sub) {
removeFromDispatchIndex(this.context, sub);
}
if (sub?.pattern.id) {
this.unsubscribe(sub.pattern.id);
Expand Down
Loading
Loading