Skip to content

Commit

Permalink
Merge pull request #46 from unyt-org/feat-improved-sync-class-constru…
Browse files Browse the repository at this point in the history
…ctors

Improved sync class constructors
  • Loading branch information
jonasstrehle authored Jan 21, 2024
2 parents bd67d88 + c2356bd commit 19851f3
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 5 deletions.
76 changes: 76 additions & 0 deletions docs/manual/12 Classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof _MyObject>

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.
10 changes: 6 additions & 4 deletions js_adapter/js_class_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<T extends Object = any> {
interface DatexClass<T extends (new (...args: unknown[]) => unknown) = (new (...args: unknown[]) => unknown), Construct = InstanceType<T>["construct"]> {

new(...args: Construct extends (...args: any) => any ? Parameters<Construct> : ConstructorParameters<T>): datexClassType<T>;

// special functions
on_result: (call: (data:any, meta:{station_id:number, station_bundle:number[]})=>any) => dc<T>;
Expand All @@ -1225,7 +1227,7 @@ type dc<T extends Record<string,any>&{new (...args:unknown[]):unknown}> = DatexC
* export type MyClass = datexClassType<typeof _MyClass>
* ```
*/
export function datexClass<T extends Record<string,any>&{new (...args:unknown[]):unknown}>(_class:T) {
export function datexClass<T extends Record<string,any>&{new (...args:any[]):any}>(_class:T) {
return <dc<ObjectRef<T>>> _class;
}

Expand Down
2 changes: 1 addition & 1 deletion runtime/pointers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1946,7 +1946,7 @@ export class Pointer<T = any> extends Ref<T> {
&& (!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()+")");
}
}

Expand Down

0 comments on commit 19851f3

Please sign in to comment.