diff --git a/docs/manual/12 Classes.md b/docs/manual/12 Classes.md index 0370115a..5c296666 100644 --- a/docs/manual/12 Classes.md +++ b/docs/manual/12 Classes.md @@ -58,5 +58,81 @@ obj.b = 15 // triggers observer obj.sum // 26 ``` +## Constructors + +The normal JavaScript class constructor gets called every time an instance of a sync class is created. +When an existing instance of a sync class is shared with another endpoint, the constructor is +called again locally on the endpoint, which is not intended but can't be prevented. + +We recommend using DATEX-compatible constructors instead, which are only ever called once at the initial creation of a sync class instance. +The DATEX constructor method is named `construct` and must be decorated with `@constructor`: + +```ts +@sync class MyObject { + @property a = 0 + @property b = 0 + + // DATEX-compatible constructor + @constructor construct() { + console.log("constructed a new MyObject") + } +} + +const obj = new MyObject() // "constructed a new MyObject" is logged +``` + +When the `obj` pointer is now accessed on a remote endpoint, the `construct` method +is not called again on the remote endpoint. + +You can also access constructor arguments like in a normal constructor: +```ts +@sync class MyObject { + @constructor construct(a: number, b: string) { + console.log("a", a) + console.log("b", a) + } +} + +const obj = new MyObject(42, 'text') +``` + +For correct typing, take a look at [this workaround](#workaround-to-get-correct-types). + +## Creating instances without `new` + +Class instances can also be created by calling the class as a function, passing +in an object with the initial property values: + +```ts +@sync class MyObject { + @property a = 0 + @property b = 0 +} + +const obj = MyObject({a: 1, b: 2}) +``` + +Currently, this results in a TypeScript error, but it runs without problems. +You can use [this workaround](#workaround-to-get-correct-types) to get rid of the TypeScript errors. + + +## Workaround to get correct types + +Currently, it is not possible to get the correct types for a sync class without some additional work. +You can add the following lines to a sync class to make the TypeScript compiler happy (this has no effect on the runtime behavior): +```ts +// sync class definition (private) +@sync class _MyObject { + @property a = 0 + @property b = 0 +} +// use these as public proxies for the actual class +export const MyObject = datexClass(_MyObject) +export type MyObject = datexClassType + +const obj1: MyObject = new MyObject() +const obj2: MyObject = MyObject({a: 1, b: 2}) +``` + ## Using the raw API For more customization, you can directly use the [JavaScript interface API] which allows you to define custom DATEX mapping behaviours for specific JavaScript types. diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index f279ab8b..c1a520d2 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -388,7 +388,7 @@ export class Decorators { type = normalizeType(params[0], false, "ext"); } else if (original_class[METADATA]?.[Decorators.FORCE_TYPE]?.constructor) type = original_class[METADATA]?.[Decorators.FORCE_TYPE]?.constructor - else type = Type.get("ext", original_class.name); + else type = Type.get("ext", original_class.name.replace(/^_/, '')); // remove leading _ from type name // return new templated class @@ -420,7 +420,7 @@ export class Decorators { type = normalizeType(params[0], false, "ext"); } else if (original_class[METADATA]?.[Decorators.FORCE_TYPE]?.constructor) type = original_class[METADATA]?.[Decorators.FORCE_TYPE]?.constructor - else type = Type.get("ext", original_class.name); + else type = Type.get("ext", original_class.name.replace(/^_/, '')); // remove leading _ from type name let callerFile:string|undefined; @@ -1198,7 +1198,9 @@ DatexFunction.setMethodMetaIndexSource(getMetaParamIndex) // new version for implemented feature functions / attributes: call datex_advanced() on the class (ideally usa as a decorator, currently not supported by ts) -interface DatexClass { +interface DatexClass unknown) = (new (...args: unknown[]) => unknown), Construct = InstanceType["construct"]> { + + new(...args: Construct extends (...args: any) => any ? Parameters : ConstructorParameters): datexClassType; // special functions on_result: (call: (data:any, meta:{station_id:number, station_bundle:number[]})=>any) => dc; @@ -1225,7 +1227,7 @@ type dc&{new (...args:unknown[]):unknown}> = DatexC * export type MyClass = datexClassType * ``` */ -export function datexClass&{new (...args:unknown[]):unknown}>(_class:T) { +export function datexClass&{new (...args:any[]):any}>(_class:T) { return >> _class; } diff --git a/runtime/pointers.ts b/runtime/pointers.ts index ea0633ec..b7586475 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -1946,7 +1946,7 @@ export class Pointer extends Ref { && (!endpoint || !Logical.matches(endpoint, this.allowed_access, Target)) && (endpoint && !Runtime.trustedEndpoints.get(endpoint.main)?.includes("protected-pointer-access")) ) { - throw new PermissionError("Endpoint has no read permissions for this pointer") + throw new PermissionError("Endpoint has no read permissions for this pointer ("+this.idString()+")"); } }