Skip to content

Commit

Permalink
support multiple tab endpoint instances, @Validate decorator, window …
Browse files Browse the repository at this point in the history
…com interface fixes
  • Loading branch information
benStre committed Nov 15, 2023
1 parent 34ffafc commit 586d5d7
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 61 deletions.
7 changes: 6 additions & 1 deletion datex_short.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { baseURL, Runtime, PrecompiledDXB, Type, Pointer, Ref, PointerProperty, primitive, any_class, Target, IdEndpoint, TransformFunctionInputs, AsyncTransformFunction, TransformFunction, TextRef, Markdown, DecimalRef, BooleanRef, IntegerRef, MinimalJSRef, RefOrValue, PartialRefOrValueObject, datex_meta, ObjectWithDatexValues, Compiler, endpoint_by_endpoint_name, endpoint_name, Storage, compiler_scope, datex_scope, DatexResponse, target_clause, ValueError, logger, Class, getUnknownMeta, Endpoint, INSERT_MARK, CollapsedValueAdvanced, CollapsedValue, SmartTransformFunction, compiler_options, activePlugins, METADATA, handleDecoratorArgs, RefOrValueObject, PointerPropertyParent, InferredPointerProperty, RefLike } from "./datex_all.ts";

/** make decorators global */
import {property as _property, sync as _sync, endpoint as _endpoint, template as _template, jsdoc as _jsdoc} from "./datex_all.ts";
import { validate as _validate, property as _property, sync as _sync, endpoint as _endpoint, template as _template, jsdoc as _jsdoc} from "./datex_all.ts";
import { effect as _effect, always as _always, toggle as _toggle, map as _map, equals as _equals, selectProperty as _selectProperty, not as _not } from "./functions.ts";
export * from "./functions.ts";
import { NOT_EXISTING, DX_SLOTS, SLOT_GET, SLOT_SET } from "./runtime/constants.ts";
Expand All @@ -17,6 +17,8 @@ export {instance} from "./js_adapter/js_class_adapter.ts";

declare global {
const property: typeof _property;
const validate: typeof _validate;

const jsdoc: typeof _jsdoc;
const sync: typeof _sync;
const endpoint: typeof _endpoint;
Expand All @@ -37,6 +39,9 @@ declare global {

// @ts-ignore global
globalThis.property = _property;
// @ts-ignore global
globalThis.validate = _validate;

// @ts-ignore global
globalThis.sync = _sync;
// @ts-ignore global
Expand Down
25 changes: 25 additions & 0 deletions js_adapter/js_class_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { DX_PERMISSIONS, DX_TYPE, INIT_PROPS } from "../runtime/constants.ts";
import { type Class } from "../utils/global_types.ts";
import { Conjunction, Disjunction, Logical } from "../types/logic.ts";
import { client_type } from "../utils/constants.ts";
import { Assertion } from "../types/assertion.ts";

const { Reflect: MetadataReflect } = client_type == 'deno' ? await import("https://deno.land/x/reflect_metadata@v0.1.12/mod.ts") : {Reflect};

Expand Down Expand Up @@ -324,6 +325,18 @@ export class Decorators {

}

/** @validate: add type assertion function */
static validate(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[((value:any)=>boolean)?] = []) {
if (kind != "field" && kind != "getter" && kind != "setter" && kind != "method") logger.error("Invalid use of @validate decorator");
else {
if (typeof params[0] !== "function") logger.error("Invalid @validate decorator value, must be a function");
else {
const assertionType = new Conjunction(Assertion.get(undefined, params[0], false));
setMetadata(Decorators.FORCE_TYPE, assertionType)
}
}
}

/** @jsdoc parse jsdoc comments and use as docs for DATEX type*/
// TODO: only works with real js decorators, otherwise line numbers don't match
static jsdoc(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[]) {
Expand Down Expand Up @@ -1158,10 +1171,22 @@ interface DatexClass<T extends Object = any> {

type dc<T extends Record<string,any>&{new (...args:unknown[]):unknown}> = DatexClass<T> & T & ((struct:InstanceType<T>) => InstanceType<T>);

/**
* Workaround to enable correct @sync class typing, until new decorators support it.
* Usage:
* ```ts
* @sync class _MyClass {}
* export const MyClass = datexClass(_MyClass)
* export type MyClass = datexClassType<typeof _MyClass>
* ```
*/
export function datexClass<T extends Record<string,any>&{new (...args:unknown[]):unknown}>(_class:T) {
return <dc<ObjectRef<T>>> _class;
}

export type datexClassType<T extends abstract new (...args: any) => any> = ObjectRef<InstanceType<T>>


/**
* @deprecated use datexClass
*/
Expand Down
6 changes: 6 additions & 0 deletions js_adapter/legacy_decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,12 @@ export function type(...args:any[]): any {
return handleDecoratorArgs(args, Decorators.type);
}

export function validate(assertion:(val:any)=>boolean):any
export function validate(...args:any[]): any {
return handleDecoratorArgs(args, Decorators.validate, true);
}


export function from(type:string|Type):any
export function from(...args:any[]): any {
return handleDecoratorArgs(args, Decorators.from);
Expand Down
1 change: 0 additions & 1 deletion network/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,6 @@ export class InterfaceManager {
for (const interfaces of CommonInterface.endpoint_connection_points.values()) {
interfaces.delete(i)
}
console.log(CommonInterface.endpoint_connection_points)

for (const interfaces of CommonInterface.indirect_endpoint_connection_points.values()) {
interfaces.delete(i)
Expand Down
80 changes: 50 additions & 30 deletions network/supranet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,21 @@ export class Supranet {
this.#connected = false;
endpoint = await this.init(endpoint, local_cache, sign_keys, enc_keys)

const shouldSwitchInstance = this.shouldSwitchInstance(endpoint);

// switching from potentially instance to another instance, make sure current endpoint is not an already active instance
if (shouldSwitchInstance && endpoint !== endpoint.main) Runtime.init(endpoint.main);

// already connected to endpoint during init
if (this.#connected && endpoint === Runtime.endpoint) {
const switched = await this.handleSwitchToInstance()
const switched = shouldSwitchInstance ? await this.handleSwitchToInstance() : false;
logger.success("Connected to the supranet as " + endpoint)
if (!switched) this.sayHelloToAllInterfaces();
return true;
}

const connected = await this._connect(via_node, !this.shouldSwitchInstance());
await this.handleSwitchToInstance()
const connected = await this._connect(via_node, !shouldSwitchInstance);
if (shouldSwitchInstance) await this.handleSwitchToInstance()

return connected;
}
Expand All @@ -100,40 +105,57 @@ export class Supranet {
}
}

private static shouldSwitchInstance() {
private static shouldSwitchInstance(endpoint: Endpoint) {
// return false;
return Runtime.endpoint.main === Runtime.endpoint && Runtime.Blockchain
return (endpoint.main === endpoint || Runtime.getActiveLocalStorageEndpoints().includes(endpoint)) && Runtime.Blockchain
}

/**
* Finds an available instance and switches endpoint
* @returns true if switched to new instance (and hello sent)
*/
private static async handleSwitchToInstance() {
if (this.shouldSwitchInstance()) {
if (!Runtime.Blockchain) {
logger.error("Cannot determine endpoint instance, blockchain not available")
}
else {
const hash = (await Storage.loadOrCreate("Datex.Supranet.ENDPOINT_INSTANCE_HASH", () => Math.random().toString(36).substring(2,18)))
try {
const instance = (await Runtime.Blockchain.getEndpointInstance(Runtime.endpoint, hash.toString()))!;
// set endpoint to instace
Runtime.init(instance);
endpoint_config.endpoint = instance;
endpoint_config.save();
this.sayHelloToAllInterfaces();
logger.success("Switched to endpoint instance " + instance)
this.onConnect();
return true;
}
catch {
logger.error("Could not determine endpoint instance (request error)");
this.sayHelloToAllInterfaces();
this.onConnect();
if (!Runtime.Blockchain) {
logger.error("Cannot determine endpoint instance, blockchain not available")
}
else {
// existing locally available endpoint instances -> hashes
const hashes = await Storage.loadOrCreate("Datex.Supranet.ENDPOINT_INSTANCE_HASHES", () => new Map<Endpoint, string>())

logger.debug("available cached instances: " + [...hashes.keys()].map(e=>e.toString()).join(", "))

const activeEndpoints = Runtime.getActiveLocalStorageEndpoints();
let hash: string|undefined = undefined;
let endpoint = Runtime.endpoint;
for (const [storedEndpoint, storedHash] of hashes) {
if (!activeEndpoints.includes(storedEndpoint)) {
hash = storedHash;
endpoint = storedEndpoint;
break;
}
}
if (!hash) hash = Math.random().toString(36).substring(2,18);

try {
const instance = (await Runtime.Blockchain.getEndpointInstance(endpoint, hash))!;
// makes sure hash is set in cache
hashes.set(instance, hash);
// set endpoint to instace
Runtime.init(instance);
endpoint_config.endpoint = instance;
endpoint_config.save();
this.sayHelloToAllInterfaces();
logger.success("Switched to endpoint instance " + instance)
this.onConnect();
return true;
}
catch {
logger.error("Could not determine endpoint instance (request error)");
this.sayHelloToAllInterfaces();
this.onConnect();
}
}

return false;
}

Expand All @@ -156,10 +178,7 @@ export class Supranet {
// Crypto.validateOwnKeysAgainstNetwork();

// send goodbye on process close
const byeDatex = <ArrayBuffer> await Datex.Compiler.compile("", [], {type:Datex.ProtocolDataType.GOODBYE, sign:true, flood:true, __routing_ttl:10})
globalThis.addEventListener("beforeunload", () => {
sendDatexViaHTTPChannel(byeDatex);
})
Runtime.goodbyeMessage = <ArrayBuffer> await Datex.Compiler.compile("", [], {type:Datex.ProtocolDataType.GOODBYE, sign:true, flood:true, __routing_ttl:10})

return connected;
}
Expand Down Expand Up @@ -338,6 +357,7 @@ export class Supranet {
public static sayHello(node:Endpoint = Runtime.main_node){
// TODO REPLACE, only temporary as placeholder to inform router about own public keys
const keys = Crypto.getOwnPublicKeysExported();
// console.warn("saying hello " + Runtime.endpoint)
Runtime.datexOut(['?', [keys], {type:ProtocolDataType.HELLO, sign:false, flood:true, __routing_ttl:10}], undefined, undefined, false, false)
// send with plain endpoint id as sender
// if (Runtime.endpoint.id_endpoint !== Runtime.endpoint) Runtime.datexOut(['?', [keys], {type:ProtocolDataType.HELLO, sign:false, flood:true, force_id:true, __routing_ttl:1}], undefined, undefined, false, false)
Expand Down
77 changes: 75 additions & 2 deletions runtime/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { createFunctionWithDependencyInjections } from "../types/function-utils.
import type { Blockchain } from "../network/blockchain_adapter.ts";
import { AutoMap } from "../utils/auto_map.ts";
import { Supranet } from "../network/supranet.ts";
import { sendDatexViaHTTPChannel } from "../network/datex-http-channel.ts";

const mime = client_type === "deno" ? (await import("https://deno.land/x/mimetypes@v1.0.0/mod.ts")).mime : null;

Expand Down Expand Up @@ -1000,7 +1001,7 @@ export class Runtime {
if (timeout > 0 && Number.isFinite(timeout)) {
setTimeout(()=>{
// reject if response wasn't already received (might still be processed, and resolve not yet called)
if (!this.callbacks_by_sid.get(unique_sid)?.[2]) reject(new NetworkError("DATEX request timeout after "+timeout+"ms: " + unique_sid + " to " + Runtime.valueToDatexStringExperimental(to)));
if (!this.callbacks_by_sid.get(unique_sid)?.[2]) reject(new NetworkError("DATEX request timeout after "+timeout+"ms: " + unique_sid + " to " + Runtime.valueToDatexString(to)));
}, timeout);
}
}
Expand Down Expand Up @@ -1066,6 +1067,74 @@ export class Runtime {
}
}

private static ownLastEndpoint?: Endpoint;
private static lastEndpointUnloadHandler?: EventListener

static goodbyeMessage?: ArrayBuffer // is set by supranet when connected

/**
* Adds endpoint to localStorage active lists
* Handles beforeunload (sending GOODBYE)
* @param endpoint
*/
static addActiveEndpoint(endpoint:Endpoint) {
let endpoints:string[] = [];
try {
endpoints = JSON.parse(localStorage['active_endpoints']) as string[]
}
catch {
localStorage['active_endpoints'] = ""
}
// remove previous local endpoint
if (this.ownLastEndpoint && endpoints.includes(this.ownLastEndpoint?.toString())) endpoints.splice(endpoints.indexOf(this.ownLastEndpoint?.toString()), 1);

// remove previous goodbye
if (this.lastEndpointUnloadHandler) {
globalThis.removeEventListener("beforeunload", this.lastEndpointUnloadHandler)
this.lastEndpointUnloadHandler = undefined;
}

// endpoint already in active list (added from other tab?)
if (endpoints.includes(endpoint.toString())) {
logger.warn("Endpoint " + endpoint + " is already active");
}
// add endpoint to active list
else {
endpoints.push(endpoint.toString())
this.ownLastEndpoint = endpoint;

this.lastEndpointUnloadHandler = () => {
// send goodbye
if (this.goodbyeMessage) sendDatexViaHTTPChannel(this.goodbyeMessage);
try {
// remove from localstorage list
endpoints = JSON.parse(localStorage['active_endpoints']) as string[]
if (endpoints.includes(endpoint?.toString())) endpoints.splice(endpoints.indexOf(endpoint?.toString()), 1);
localStorage['active_endpoints'] = JSON.stringify(endpoints)
}
catch {
localStorage['active_endpoints'] = ""
}
}

// delete endpoint on exit
globalThis.addEventListener("beforeunload", this.lastEndpointUnloadHandler);
}

localStorage['active_endpoints'] = JSON.stringify(endpoints)
}

static getActiveLocalStorageEndpoints() {
try {
const endpoints = JSON.parse(localStorage['active_endpoints']) as string[]
return endpoints.map((e) => Target.get(e) as Endpoint).filter((e) => e!==this.ownLastEndpoint)
}
catch {
localStorage['active_endpoints'] = ""
return []
}
}

/**
* Creates default static scopes
* + other async initializations
Expand All @@ -1074,6 +1143,10 @@ export class Runtime {
*/
public static init(endpoint?:Endpoint) {

// save all currently active endpoints for shared local storage (multiple tabs)
if (endpoint && endpoint != LOCAL_ENDPOINT && client_type == "browser") this.addActiveEndpoint(endpoint)


if (endpoint) Runtime.endpoint = endpoint;

if (this.initialized) return;
Expand Down Expand Up @@ -1581,7 +1654,7 @@ export class Runtime {
private static receivedMessagesHistory:string[] = []

private static async checkDuplicate(header: dxb_header) {
const identifier = `${header.sender}:${header.sid}:${header.inc}:${header.return_index}:${await Compiler.getValueHashString(header.routing?.receivers)}`;
const identifier = `${header.type}:${header.sender}:${header.sid}:${header.inc}:${header.return_index}:${await Compiler.getValueHashString(header.routing?.receivers)}`;
let isDuplicate = false;
// is duplicate
if (this.receivedMessagesHistory.includes(identifier)) {
Expand Down
12 changes: 7 additions & 5 deletions runtime/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,23 +259,25 @@ export class Storage {
for (const [key, val] of Storage.cache) {
try {
c++;
this.setItem(key, val, true, location);
} catch (e) {console.error(e)}
const res = this.setItem(key, val, true, location);
if (res instanceof Promise) res.catch(()=>{})
} catch (e) {}
}

// update pointers
for (const ptr of this.#storage_active_pointers) {
try {
c++;
this.setPointer(ptr, true, location);
} catch (e) {console.error(e)}
const res = this.setPointer(ptr, true, location);
if (res instanceof Promise) res.catch(()=>{})
} catch (e) {}
}
for (const id of this.#storage_active_pointer_ids) {
try {
c++;
const ptr = Pointer.get(id);
if (ptr?.value_initialized) this.setPointer(ptr, true, location);
} catch (e) {console.error(e)}
} catch (e) {}
}

this.updateSaveTime(location); // last full backup to this storage location
Expand Down
4 changes: 2 additions & 2 deletions types/assertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export class Assertion<T=any> extends ExtensibleFunction implements ValueConsume
assert<B extends boolean = false>(value:T|Tuple<T>, SCOPE?:datex_scope, return_boolean:B = false): B extends true ? boolean|Promise<boolean> : void|Promise<void> {
// ntarget
if (this.ntarget) {
if (this.ntarget_async) return this.checkResultPromise(<Promise<string | boolean>>this.ntarget(...(value instanceof Tuple ? value.toArray() : value)), return_boolean)
else return this.checkResult(<string | boolean>this.ntarget(...(value instanceof Tuple ? value.toArray() : value)), return_boolean)
if (this.ntarget_async) return this.checkResultPromise(<Promise<string | boolean>>this.ntarget(...(value instanceof Tuple ? value.toArray() : (value instanceof Array ? value : [value]))), return_boolean)
else return this.checkResult(<string | boolean>this.ntarget(...(value instanceof Tuple ? value.toArray() : (value instanceof Array ? value : [value]))), return_boolean)
}

// datex
Expand Down
2 changes: 1 addition & 1 deletion types/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ export class Function<T extends (...args: any) => any = (...args: any) => any> e
if (Number(key.toString()) < 0) logger.warn(Datex.Pointer.getByValue(this)?.idString() + ": Invalid function arguments: '" + key + "'");
else if (Number(key.toString()) >= this.params.size) {
// ignore if no params (TODO: just workaround to prevent errors)
if (this.params.size !== 0) logger.warn(Datex.Pointer.getByValue(this)?.idString()+": Maximum number of function arguments is " + (this.params.size));
// if (this.params.size !== 0) logger.warn(Datex.Pointer.getByValue(this)?.idString()+": Maximum number of function arguments is " + (this.params.size));
}
params[Number(key.toString())] = val;
}
Expand Down
Loading

0 comments on commit 586d5d7

Please sign in to comment.