diff --git a/datex_short.ts b/datex_short.ts index e201268f..80be4be1 100644 --- a/datex_short.ts +++ b/datex_short.ts @@ -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"; @@ -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; @@ -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 diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index cde98190..c3b5ebe8 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -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}; @@ -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:[]) { @@ -1158,10 +1171,22 @@ interface DatexClass { type dc&{new (...args:unknown[]):unknown}> = DatexClass & T & ((struct:InstanceType) => InstanceType); +/** + * 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 + * ``` + */ export function datexClass&{new (...args:unknown[]):unknown}>(_class:T) { return >> _class; } +export type datexClassType any> = ObjectRef> + + /** * @deprecated use datexClass */ diff --git a/js_adapter/legacy_decorators.ts b/js_adapter/legacy_decorators.ts index 286f9c28..6f23f9ff 100644 --- a/js_adapter/legacy_decorators.ts +++ b/js_adapter/legacy_decorators.ts @@ -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); diff --git a/network/client.ts b/network/client.ts index 71c5dd22..6e8c564a 100644 --- a/network/client.ts +++ b/network/client.ts @@ -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) diff --git a/network/supranet.ts b/network/supranet.ts index 04ca9549..9f149e98 100644 --- a/network/supranet.ts +++ b/network/supranet.ts @@ -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; } @@ -100,9 +105,9 @@ 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 } /** @@ -110,30 +115,47 @@ export class Supranet { * @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()) + + 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; } @@ -156,10 +178,7 @@ export class Supranet { // Crypto.validateOwnKeysAgainstNetwork(); // send goodbye on process close - const byeDatex = await Datex.Compiler.compile("", [], {type:Datex.ProtocolDataType.GOODBYE, sign:true, flood:true, __routing_ttl:10}) - globalThis.addEventListener("beforeunload", () => { - sendDatexViaHTTPChannel(byeDatex); - }) + Runtime.goodbyeMessage = await Datex.Compiler.compile("", [], {type:Datex.ProtocolDataType.GOODBYE, sign:true, flood:true, __routing_ttl:10}) return connected; } @@ -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) diff --git a/runtime/runtime.ts b/runtime/runtime.ts index 12a7fde7..ddfb6b3d 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -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; @@ -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); } } @@ -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 @@ -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; @@ -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)) { diff --git a/runtime/storage.ts b/runtime/storage.ts index f520fdc4..1f95f190 100644 --- a/runtime/storage.ts +++ b/runtime/storage.ts @@ -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 diff --git a/types/assertion.ts b/types/assertion.ts index c809d58a..8cbeedd2 100644 --- a/types/assertion.ts +++ b/types/assertion.ts @@ -38,8 +38,8 @@ export class Assertion extends ExtensibleFunction implements ValueConsume assert(value:T|Tuple, SCOPE?:datex_scope, return_boolean:B = false): B extends true ? boolean|Promise : void|Promise { // ntarget if (this.ntarget) { - if (this.ntarget_async) return this.checkResultPromise(>this.ntarget(...(value instanceof Tuple ? value.toArray() : value)), return_boolean) - else return this.checkResult(this.ntarget(...(value instanceof Tuple ? value.toArray() : value)), return_boolean) + if (this.ntarget_async) return this.checkResultPromise(>this.ntarget(...(value instanceof Tuple ? value.toArray() : (value instanceof Array ? value : [value]))), return_boolean) + else return this.checkResult(this.ntarget(...(value instanceof Tuple ? value.toArray() : (value instanceof Array ? value : [value]))), return_boolean) } // datex diff --git a/types/function.ts b/types/function.ts index 512a2b51..7b09cdac 100644 --- a/types/function.ts +++ b/types/function.ts @@ -334,7 +334,7 @@ export class Function 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; } diff --git a/types/logic.ts b/types/logic.ts index 2d4a615b..6029bd5d 100644 --- a/types/logic.ts +++ b/types/logic.ts @@ -61,7 +61,7 @@ export class Logical extends Set { // value clause matches against other clause // if no atomic_class is provided, the first value clause literal is used to determine a atomic class - public static matches(value:clause, against:clause, atomic_class?:Class&LogicalComparator): boolean { + public static matches(value:clause, against:clause, atomic_class?:Class&LogicalComparator, assertionValue = value): boolean { // TODO: empty - does not match? if (against === undefined) return false; @@ -111,11 +111,11 @@ export class Logical extends Set { } // default - return this.matchesSingle(Ref.collapseValue(value, true, true), against, atomic_class); + return this.matchesSingle(Ref.collapseValue(value, true, true), against, atomic_class, assertionValue); } - private static matchesSingle(atomic_value:T, against: clause, atomic_class:Class&LogicalComparator): boolean { + private static matchesSingle(atomic_value:T, against: clause, atomic_class:Class&LogicalComparator, assertionValue = atomic_value): boolean { atomic_value = Datex.Ref.collapseValue(atomic_value, true, true); against = > Datex.Ref.collapseValue(against, true, true); @@ -128,25 +128,25 @@ export class Logical extends Set { // TODO:empty disjunction == any? if (against.size == 0) return true; for (const t of against) { - if (this.matchesSingle(atomic_value, t, atomic_class)) return true; // any type matches + if (this.matchesSingle(atomic_value, t, atomic_class, assertionValue)) return true; // any type matches } return false; } // and if (against instanceof Conjunction) { for (const t of against) { - if (!this.matchesSingle(atomic_value, t, atomic_class)) return false; // any type does not match + if (!this.matchesSingle(atomic_value, t, atomic_class, assertionValue)) return false; // any type does not match } return true; } // not if (against instanceof Negation) { - return !this.matchesSingle(atomic_value, against.not(), atomic_class) + return !this.matchesSingle(atomic_value, against.not(), atomic_class, assertionValue) } // assertion if (against instanceof Assertion) { - const res = against.assert(atomic_value, undefined, true); + const res = against.assert(assertionValue, undefined, true); if (res instanceof Promise) throw new RuntimeError("async assertion cannot be evaluated in logical connective"); return res } diff --git a/types/storage_map.ts b/types/storage_map.ts index f6853f96..252e6ae4 100644 --- a/types/storage_map.ts +++ b/types/storage_map.ts @@ -21,12 +21,13 @@ export class StorageWeakMap { #prefix?: string; constructor(){ - Pointer.proxifyValue(this) + // TODO: does not work with eternal pointers! + // Pointer.proxifyValue(this) } static async from(entries: readonly (readonly [K, V])[]){ - const map = new StorageWeakMap(); + const map = $$(new StorageWeakMap()); for (const [key, value] of entries) await map.set(key, value); return map; } @@ -101,7 +102,7 @@ export class StorageWeakMap { export class StorageMap extends StorageWeakMap { static override async from(entries: readonly (readonly [K, V])[]){ - const map = new StorageMap(); + const map = $$(new StorageMap()); for (const [key, value] of entries) await map.set(key, value); return map; } diff --git a/types/storage_set.ts b/types/storage_set.ts index 11e4bd02..5c4116fd 100644 --- a/types/storage_set.ts +++ b/types/storage_set.ts @@ -17,11 +17,12 @@ export class StorageSet { #prefix?: string; constructor(){ - Pointer.proxifyValue(this) + // TODO: does not work with eternal pointers! + // Pointer.proxifyValue(this) } static async from(values: readonly V[]) { - const set = new StorageSet(); + const set = $$(new StorageSet()); for (const v of values) await set.add(v); return set; } diff --git a/types/type.ts b/types/type.ts index b1778b50..1a999f93 100644 --- a/types/type.ts +++ b/types/type.ts @@ -184,7 +184,7 @@ export class Type extends ExtensibleFunction { assign_to_object[key] = value[key]; } // check value type - else if (key in value && required_type.matches(value[key])) { + else if (key in value && Type.matches(value[key], required_type)) { assign_to_object[key] = value[key]; } // JS number->bigint conversion @@ -405,7 +405,7 @@ export class Type extends ExtensibleFunction { public isPropertyValueAllowed(property:any, value:any) { if (!this.#template) return true; else if (typeof property !== "string") return true; // only strings handled by templates - else return (!this.#template[property] || this.#template[property].matches?.(value)) // check if value allowed + else return (!this.#template[property] || Type.matches(value, this.#template[property])) // check if value allowed } // get type for value in template @@ -640,8 +640,8 @@ export class Type extends ExtensibleFunction { // type check (type is a subtype of matches_type) // TODO: swap arguments - public static matchesType(type:type_clause, against: type_clause) { - return Logical.matches(type, against, Type); + public static matchesType(type:type_clause, against: type_clause, assertionValue?:any) { + return Logical.matches(type, against, Type, assertionValue); } @@ -669,11 +669,10 @@ export class Type extends ExtensibleFunction { return value.length <= type.parameters[0]; } - return Type.matchesType(Type.ofValue(value), type); + return Type.matchesType(Type.ofValue(value), type, value); } public static extends(type:Type, extends_type:type_clause){ - console.log("extemds",type,extends_type) return type!=extends_type && Type.matchesType(type, extends_type); } diff --git a/windows/window-com-interface.ts b/windows/window-com-interface.ts index 31b20446..551f4cfb 100644 --- a/windows/window-com-interface.ts +++ b/windows/window-com-interface.ts @@ -73,7 +73,7 @@ export class WindowCommunicationInterface extends CommonInterface<[Window, strin } public override disconnect() { - + globalThis.removeEventListener("message", this.onReceive); } get other() {