From 2e15ff8590f5aed658ad557bf3dd695cd509a40f Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 13 Feb 2024 12:58:33 +0100 Subject: [PATCH 01/56] add iterators --- storage/storage-locations/sql-db.ts | 38 +++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 33b04242..8705b638 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -92,6 +92,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #query(query_string:string, query_params?:any[]): Promise { + await this.#init(); // handle arraybuffers if (query_params) { @@ -330,20 +331,47 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .build() )); console.log("encoded",encoded) - if (!encoded) return null; - else return Runtime.decodeValue(encoded, false, conditions); + if (!encoded.value) return null; + else return Runtime.decodeValue(encoded.value, false, conditions); } async hasItem(key:string) { - return false + const count = (await this.#queryFirst<{COUNT: number}>( + new Query() + .table(this.#metaTables.items.name) + .select("COUNT(*) as COUNT") + .where(Where.eq("key", key)) + .build() + )); + return count.COUNT > 0; } async getItemKeys() { - return function*(){}() + const keys = []/*await this.#query<{key:string}>( + new Query() + .table(this.#metaTables.items.name) + .select("key") + .build() + )*/ + return function*(){ + for (const {key} of keys) { + yield key; + } + }() } async getPointerIds() { - return function*(){}() + const pointerIds = []/*await this.#query<{_ptr_id:string}>( + new Query() + .table(this.#metaTables.pointerMapping.name) + .select(this.#pointerMysqlColumnName) + .build() + )*/ + return function*(){ + for (const {_ptr_id} of pointerIds) { + yield _ptr_id; + } + }() } async removeItem(key: string): Promise { From 18ca282f752c64e1416edea34dbbb7ab673ca1ec Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 13 Feb 2024 18:15:39 +0100 Subject: [PATCH 02/56] support classes in type decorator --- js_adapter/js_class_adapter.ts | 9 +++++++-- js_adapter/legacy_decorators.ts | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index f56f445b..9cdc44f9 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -485,7 +485,7 @@ export class Decorators { /** @type(type:string|DatexType)/ (namespace:name) * sync class with type */ - static type(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[(string|Type)] = []) { + static type(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[(string|Type|Class)] = []) { const type = normalizeType(params[0]); setMetadata(Decorators.FORCE_TYPE, type) } @@ -571,7 +571,7 @@ export class Decorators { * @param allowTypeParams * @returns */ -function normalizeType(type:Type|string, allowTypeParams = true, defaultNamespace = "std") { +function normalizeType(type:Type|string|Class, allowTypeParams = true, defaultNamespace = "std") { if (typeof type == "string") { // extract type name and parameters const [typeName, paramsString] = type.replace(/^\$/,'').match(/^((?:[\w-]+\:)?[\w-]*)(?:\((.*)\))?$/)?.slice(1) ?? []; @@ -587,6 +587,11 @@ function normalizeType(type:Type|string, allowTypeParams = true, defaultNamespac if (!allowTypeParams && type.parameters?.length) throw new Error(`Type parameters not allowed (${type})`); return type } + else if (typeof type == "function") { + const classType = Type.getClassDatexType(type) + if (!classType) throw new Error("Could not get a DATEX type for class " + type.name + ". Only @sync classes can be used as types"); + return classType + } else { console.error("invalid type",type) throw new Error("Invalid type") diff --git a/js_adapter/legacy_decorators.ts b/js_adapter/legacy_decorators.ts index 6df45eb2..18f7b847 100644 --- a/js_adapter/legacy_decorators.ts +++ b/js_adapter/legacy_decorators.ts @@ -15,6 +15,7 @@ import { } from "../runtime/runtime.ts"; import { endpoint_name, Target, target_clause } from "../types/addressing.ts"; import { Type } from "../types/type.ts"; import { UpdateScheduler, Pointer } from "../runtime/pointers.ts"; +import { Class } from "../utils/global_types.ts"; // decorator types export type context_kind = 'class'|'method'|'getter'|'setter'|'field'|'auto-accessor'; @@ -286,7 +287,7 @@ export function anonymous(...args:any[]) { } -export function type(type:string|Type):any +export function type(type:string|Type|Class):any export function type(...args:any[]): any { return handleDecoratorArgs(args, Decorators.type, true); } From 4435746d99c988a57b3a9732e207429811c8446f Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 13 Feb 2024 18:15:51 +0100 Subject: [PATCH 03/56] update decompiler --- wasm/adapter/pkg/datex_wasm_bg.wasm | Bin 805994 -> 805984 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/wasm/adapter/pkg/datex_wasm_bg.wasm b/wasm/adapter/pkg/datex_wasm_bg.wasm index 78c62bee020917ac4cb76a2df52ebbbea0d3a38b..ae7dfda09ddca92eb7b541a0f9a669ee8b6d020a 100644 GIT binary patch delta 94333 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 NLDf8^&(=}*KLAW4x4HlT delta 94647 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

IB5_K%oQbamUaJLDa;7LNtPtJZp{= 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 From 4ce222494320568a8b26ffc2cf75a34ca290daf1 Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 13 Feb 2024 18:16:59 +0100 Subject: [PATCH 04/56] print storage collection items without loading pointers into RAM --- storage/storage.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/storage/storage.ts b/storage/storage.ts index f94b0814..b0c626ea 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -907,7 +907,7 @@ export class Storage { } - public static async getPointerKeys(location?:StorageLocation){ + public static async getPointerIds(location?:StorageLocation){ // for specific location if (location) return location.getPointerIds(); @@ -938,7 +938,7 @@ export class Storage { const promises = []; - for (const pointer_id of await this.getPointerKeys(from)) { + for (const pointer_id of await this.getPointerIds(from)) { const buffer = await from.getPointerValueDXB(pointer_id); if (!buffer) logger.error("could not copy empty pointer value: " + pointer_id) else promises.push(to.setPointerValueDXB(pointer_id, buffer)) @@ -1291,7 +1291,7 @@ export class Storage { public static async getSnapshot(options: StorageSnapshotOptions = {internalItems: false, expandStorageMapsAndSets: true}) { const items = await this.createSnapshot(this.getItemKeys.bind(this), this.getItemDecompiled.bind(this)); - const pointers = await this.createSnapshot(this.getPointerKeys.bind(this), this.getPointerDecompiledFromLocation.bind(this)); + const pointers = await this.createSnapshot(this.getPointerIds.bind(this), this.getPointerDecompiledFromLocation.bind(this)); // remove keys items that are unrelated to normal storage for (const [key] of items.snapshot) { @@ -1319,18 +1319,30 @@ export class Storage { } if (ptr.val instanceof StorageMap) { const map = ptr.val; + const keyIterator = await this.getItemKeysStartingWith((map as any)._prefix) let inner = ""; - for await (const [key, val] of map) { - inner += ` ${Runtime.valueToDatexStringExperimental(key, true, true)}\x1b[0m => ${Runtime.valueToDatexStringExperimental(val, true, true)}\n` + for await (const key of keyIterator) { + const valString = await this.getItemDecompiled(key, true, location); + if (valString === NOT_EXISTING) { + logger.error("Invalid entry in storage (" + location.name + "): " + key); + continue; + } + inner += ` ${Runtime.valueToDatexStringExperimental(key, true, true)}\x1b[0m => ${valString}\n` } // substring: remove last \n if (inner) storageMap.set(location, "\x1b[38;2;50;153;220m \x1b[0m{\n"+inner.substring(0, inner.length-1)+"\x1b[0m\n}") } else if (ptr.val instanceof StorageSet) { const set = ptr.val; + const keyIterator = await this.getItemKeysStartingWith((set as any)._prefix) let inner = ""; - for await (const val of set) { - inner += ` ${Runtime.valueToDatexStringExperimental(val, true, true)},\n` + for await (const key of keyIterator) { + const valString = await this.getItemDecompiled(key, true, location); + if (valString === NOT_EXISTING) { + logger.error("Invalid entry in storage (" + location.name + "): " + key); + continue; + } + inner += ` ${valString},\n` } // substring: remove last \n if (inner) storageMap.set(location, "\x1b[38;2;50;153;220m \x1b[0m{\n"+inner.substring(0, inner.length-1)+"\x1b[0m\n}") From a9ae0a6839cbb04f172262f91a4f51a965ca59ea Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 13 Feb 2024 18:17:09 +0100 Subject: [PATCH 05/56] updates to sql db --- storage/storage-locations/sql-db.ts | 301 +++++++++++++++++++++++----- 1 file changed, 250 insertions(+), 51 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 8705b638..7a3aef40 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -19,6 +19,8 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { name = "SQL_DB" #connected = false; + #initializing = false + #initialized = false #options: dbOptions #sqlClient: Client|undefined @@ -43,7 +45,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { rawPointers: { name: "datex_pointers_raw", columns: [ - ["id", "varchar(50)", "PRIMARY KEY"], + [this.#pointerMysqlColumnName, "varchar(50)", "PRIMARY KEY"], ["value", "blob"] ] }, @@ -56,6 +58,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } satisfies Record; + // cached table columns #tableColumns = new Map>() constructor(options:dbOptions, private log?:(...args:unknown[])=>void) { @@ -70,44 +73,49 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #init() { + if (this.#initialized) return; + this.#initializing = true; await this.#connect(); await this.#setupMetaTables(); + this.#initializing = false; + this.#initialized = true; } - async resetAll() { - await this.#init(); - + async #resetAll() { + // drop all custom type tables const tables = await this.#query<{table_name:string}>( new Query() .table(this.#metaTables.typeMapping.name) .select("table_name") .build() ) - const tableNames = tables.map(({table_name})=>'`'+table_name+'`') - await this.#query<{table_name:string}>(`DROP TABLE IF EXISTS ${tableNames.join(',')};`) - await this.#query<{table_name:string}>(`TRUNCATE TABLE ${this.#metaTables.typeMapping.name};`) - await this.#query<{table_name:string}>(`TRUNCATE TABLE ${this.#metaTables.pointerMapping.name};`) + + // truncate meta tables + for (const table of Object.values(this.#metaTables)) { + await this.#query<{table_name:string}>(`TRUNCATE TABLE ${table.name};`) + } } async #query(query_string:string, query_params?:any[]): Promise { - await this.#init(); + // prevent infinite recursion if calling query from within init() + if (!this.#initializing) await this.#init(); // handle arraybuffers if (query_params) { for (let i = 0; i < query_params.length; i++) { const param = query_params[i]; if (param instanceof ArrayBuffer) { - query_params[i] = new TextDecoder().decode(param) + query_params[i] = this.#binaryToString(param) } if (param instanceof Array) { - query_params[i] = param.map(p => p instanceof ArrayBuffer ? new TextDecoder().decode(p) : p) + query_params[i] = param.map(p => p instanceof ArrayBuffer ? this.#binaryToString(p) : p) } } } - // console.log("QUERY: " + query_string, query_params) + console.log("QUERY: " + query_string, query_params) if (typeof query_string != "string") {console.error("invalid query:", query_string); throw new Error("invalid query")} if (!query_string) throw new Error("empty query"); @@ -121,12 +129,19 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - async #queryFirst(query_string:string, query_params?:any[]): Promise { + #stringToBinary(value: string){ + return Uint8Array.from(value, x => x.charCodeAt(0)).buffer + } + #binaryToString(value: ArrayBuffer){ + return String.fromCharCode.apply(null, new Uint8Array(value) as unknown as number[]) + } + + async #queryFirst(query_string:string, query_params?:any[]): Promise { return (await this.#query(query_string, query_params))?.[0] } async #createTableIfNotExists(definition: TableDefinition) { - const exists = await this.#queryFirst( + const exists = this.#tableColumns.has(definition.name) || await this.#queryFirst( new Query() .table("information_schema.tables") .select("*") @@ -152,7 +167,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { new Query() .table(this.#metaTables.typeMapping.name) .select("table_name") - .where(Where.eq("type", type.toString())) + .where(Where.eq("type", this.#typeToString(type))) .build() ))?.table_name; if (!existingTable) { @@ -161,10 +176,26 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { else return existingTable } + async #getTypeForTable(table: string) { + const type = await this.#queryFirst<{type:string}>( + new Query() + .table(this.#metaTables.typeMapping.name) + .select("type") + .where(Where.eq("table_name", table)) + .build() + ) + if (!type) return null; + return Datex.Type.get(type.type) + } + #typeToTableName(type: Datex.Type) { return type.namespace=="ext" ? type.name : `${type.namespace}_${type.name}`; } + #typeToString(type: Datex.Type) { + return type.namespace + ":" + type.name; + } + async #createTableForType(type: Datex.Type) { const columns:ColumnDefinition[] = [ [this.#pointerMysqlColumnName, this.#pointerMysqlType, 'PRIMARY KEY INVISIBLE DEFAULT "0"'] @@ -220,7 +251,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { new Query() .table(this.#metaTables.typeMapping.name) .insert({ - type: type.toString(), + type: this.#typeToString(type), table_name: name }) .build() @@ -281,8 +312,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } // this.log("cols", insertData) - await this.#query('INSERT INTO ?? ?? VALUES ?;', [table, Object.keys(insertData), Object.values(insertData)]) + + // add to pointer mapping + await this.#updatePointerMapping(pointer.id, table) } async #updatePointer(pointer: Datex.Pointer, keys:string[]) { @@ -305,7 +338,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { pointer.id ] ); - return exists.COUNT > 0; + return (!!exists) && exists.COUNT > 0; } @@ -314,25 +347,16 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async setItem(key: string,value: unknown) { - await this.#init(); const dependencies = new Set() const encoded = Compiler.encodeValue(value, dependencies); console.log("db set item", key) - await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.items.name, ["key", "value"], [key, encoded], encoded]) - // await datex_item_storage.setItem(key, Compiler.encodeValue(value, dependencies)); + await this.setItemValueDXB(key, encoded) return dependencies; } async getItem(key: string, conditions: ExecConditions): Promise { - const encoded = (await this.#queryFirst<{value: ArrayBuffer}>( - new Query() - .table(this.#metaTables.items.name) - .select("value") - .where(Where.eq("key", key)) - .build() - )); - console.log("encoded",encoded) - if (!encoded.value) return null; - else return Runtime.decodeValue(encoded.value, false, conditions); + const encoded = await this.getItemValueDXB(key); + if (!encoded) return null; + return Runtime.decodeValue(encoded, false, conditions); } async hasItem(key:string) { @@ -343,16 +367,16 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .where(Where.eq("key", key)) .build() )); - return count.COUNT > 0; + return (!!count) && count.COUNT > 0; } async getItemKeys() { - const keys = []/*await this.#query<{key:string}>( + const keys = await this.#query<{key:string}>( new Query() .table(this.#metaTables.items.name) .select("key") .build() - )*/ + ) return function*(){ for (const {key} of keys) { yield key; @@ -361,12 +385,12 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async getPointerIds() { - const pointerIds = []/*await this.#query<{_ptr_id:string}>( + const pointerIds = await this.#query<{_ptr_id:string}>( new Query() .table(this.#metaTables.pointerMapping.name) .select(this.#pointerMysqlColumnName) .build() - )*/ + ) return function*(){ for (const {_ptr_id} of pointerIds) { yield _ptr_id; @@ -375,20 +399,27 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async removeItem(key: string): Promise { - + await this.#query('DELETE FROM ?? WHERE ??=?;', [this.#metaTables.items.name, "key", key]) } async getItemValueDXB(key: string): Promise { - + const encoded = (await this.#queryFirst<{value: string}>( + new Query() + .table(this.#metaTables.items.name) + .select("value") + .where(Where.eq("key", key)) + .build() + )); + if (!encoded || !encoded.value) return null; + else return this.#stringToBinary(encoded.value); } async setItemValueDXB(key: string, value: ArrayBuffer) { - + const stringBinary = this.#binaryToString(value) + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.items.name, ["key", "value"], [key, stringBinary], stringBinary]) } async setPointer(pointer: Pointer, partialUpdateKey: unknown|typeof NOT_EXISTING): Promise>> { const dependencies = new Set() - await this.#init(); - // is templatable pointer type if (pointer.type.template) { this.log?.("update " + pointer.id + " - " + pointer.type, partialUpdateKey, await this.#pointerEntryExists(pointer)) @@ -402,7 +433,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (typeof partialUpdateKey !== "string") throw new Error("invalid key type for SQL table: " + Datex.Type.ofValue(partialUpdateKey)) await this.#updatePointer(pointer, [partialUpdateKey]) } - // full udpdate + // full update else { // TODO } @@ -419,33 +450,201 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async setPointerRaw(pointer: Pointer) { console.log("storing raw pointer: " + Runtime.valueToDatexStringExperimental(pointer, true, true)) - await this.#init(); const dependencies = new Set() const encoded = Compiler.encodeValue(pointer, dependencies, true, false, true); - await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, ["id", "value"], [pointer.id, encoded], encoded]) - // await datex_item_storage.setItem(key, Compiler.encodeValue(value, dependencies)); + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointer.id, encoded], encoded]) + // add to pointer mapping + await this.#updatePointerMapping(pointer.id, this.#metaTables.rawPointers.name) + // await datex_item_storage.setItem(key, Compiler.encodeValue(value, dependencies)); return dependencies; } + async #updatePointerMapping(pointerId: string, tableName: string) { + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE table_name=?;', [this.#metaTables.pointerMapping.name, [this.#pointerMysqlColumnName, "table_name"], [pointerId, tableName], tableName]) + } + async getPointerValue(pointerId: string, outer_serialized: boolean): Promise { - + // get table where pointer is stored + const table = await this.#getPointerTable(pointerId); + console.log("table for pointer", pointerId, table) + if (!table) { + logger.error("No table found for pointer " + pointerId); + return null; + } + + // is raw pointer + if (table == this.#metaTables.rawPointers.name) { + const value = (await this.#queryFirst<{value: string}>( + new Query() + .table(this.#metaTables.rawPointers.name) + .select("value") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .build() + ))?.value; + return value ? Runtime.decodeValue(this.#stringToBinary(value), outer_serialized) : null; + } + + // is templated pointer + else { + const type = await this.#getTypeForTable(table); + if (!type) { + logger.error("No type found for table " + table); + return null; + } + const object = await this.#getTemplatedPointerObject(pointerId, table); + if (!object) return null; + + // resolve foreign pointers + for (const [colName, {foreignPtr}] of this.#tableColumns.get(table)!.entries()) { + // is an object type with a template + if (foreignPtr) { + if (typeof object[colName] == "string") { + object[colName] = await this.getPointerValue(object[colName] as string, false); + } + else { + logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) + } + } + } + console.log("Templateo",object) + return type.cast(object, undefined, undefined, false); + } } - async removePointer(pointerId: string): Promise { + async #getTemplatedPointerValueString(pointerId: string, table?: string) { + table = table ?? await this.#getPointerTable(pointerId); + if (!table) { + logger.error("No table found for pointer " + pointerId); + return null; + } + + const type = await this.#getTypeForTable(table); + if (!type) { + logger.error("No type found for table " + table); + return null; + } + + const object = await this.#getTemplatedPointerObject(pointerId, table); + if (!object) return null; + + // resolve foreign pointers + const foreignPointerPlaceholders: string[] = [] + // const foreignPointerPlaceholderPromises: Promise[] = [] + for (const [colName, {foreignPtr}] of this.#tableColumns.get(table)!.entries()) { + // is an object type with a template + if (foreignPtr) { + if (typeof object[colName] == "string") { + const ptrId = object[colName] as string + object[colName] = `\u0001${foreignPointerPlaceholders.length}` + foreignPointerPlaceholders.push("$"+ptrId) + } + else { + logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) + } + } + } + + // const foreignPointerPlaceholders = await Promise.all(foreignPointerPlaceholderPromises) + + const objectString = Datex.Runtime.valueToDatexStringExperimental(object, false, false) + .replace(/"\u0001(\d+)"/g, (_, index) => foreignPointerPlaceholders[parseInt(index)]??"void") + + return `${type.toString()} ${objectString}` + } + + async #getTemplatedPointerObject(pointerId: string, table?: string) { + table = table ?? await this.#getPointerTable(pointerId); + if (!table) { + logger.error("No table found for pointer " + pointerId); + return null; + } + const object = await this.#queryFirst>( + new Query() + .table(table) + .select("*") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .build() + ) + if (!object) return null; + const type = await this.#getTypeForTable(table); + if (!type) { + logger.error("No type found for table " + table); + return null; + } + return object; + } + + async #getTemplatedPointerValueDXB(pointerId: string, table?: string) { + const string = await this.#getTemplatedPointerValueString(pointerId, table); + if (!string) return null; + console.log("string: " + string) + const compiled = await Compiler.compile(string, [], {sign: false, encrypt: false, to: Datex.Runtime.endpoint}, false) as ArrayBuffer; + return compiled + } + + async removePointer(pointerId: string): Promise { + // get table where pointer is stored + const table = await this.#getPointerTable(pointerId); + if (table) { + await this.#query('DELETE FROM ?? WHERE ??=?;', [table, this.#pointerMysqlColumnName, pointerId]) + } + // delete from pointer mapping + await this.#query('DELETE FROM ?? WHERE ??=?;', [this.#metaTables.pointerMapping.name, this.#pointerMysqlColumnName, pointerId]) } + async getPointerValueDXB(pointerId: string): Promise { - + // get table where pointer is stored + const table = await this.#getPointerTable(pointerId); + console.log("table for pointer", pointerId, table) + + // is raw pointer + if (table == this.#metaTables.rawPointers.name) { + const value = (await this.#queryFirst<{value: string}>( + new Query() + .table(this.#metaTables.rawPointers.name) + .select("value") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .build() + ))?.value; + return value ? this.#stringToBinary(value) : null; + } + + // is templated pointer + else { + return this.#getTemplatedPointerValueDXB(pointerId, table); + } + + } + + async #getPointerTable(pointerId: string) { + return (await this.#queryFirst<{table_name:string}>( + new Query() + .table(this.#metaTables.pointerMapping.name) + .select("table_name") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .build() + ))?.table_name; } - async setPointerValueDXB(pointerId: string, value: ArrayBuffer) { + async setPointerValueDXB(pointerId: string, value: ArrayBuffer) { + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointerId, value], value]) + // add to pointer mapping + await this.#updatePointerMapping(pointerId, this.#metaTables.rawPointers.name) } async hasPointer(pointerId: string): Promise { - return false; + const count = (await this.#queryFirst<{COUNT: number}>( + new Query() + .table(this.#metaTables.pointerMapping.name) + .select("COUNT(*) as COUNT") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .build() + )); + return (!!count) && count.COUNT > 0; } async clear() { - // TODO! + await this.#resetAll(); } } \ No newline at end of file From deca8562229aee759a300abdb2af7ac0f6ddd4a1 Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 13 Feb 2024 19:00:32 +0100 Subject: [PATCH 06/56] set default falsy value for toggle to null --- functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions.ts b/functions.ts index 7478ad24..2fdedfbd 100644 --- a/functions.ts +++ b/functions.ts @@ -325,7 +325,7 @@ export function map(iterable: Iterable< * @param if_true value selected if true * @param if_false value selected if false */ -export function toggle(value:RefLike, if_true:T, if_false:T): MinimalJSRef { +export function toggle(value:RefLike, if_true:T, if_false:T = null as T): MinimalJSRef { return transform([value], v=>v?if_true:if_false, // dx transforms not working correctly (with uix) /*` From f711fbdfd4fa7725bfd35cbd29dfd3d465d3b244 Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 14 Feb 2024 00:14:28 +0100 Subject: [PATCH 07/56] improve sql storage location --- storage/storage-locations/sql-db.ts | 244 +++++++++++++++------------- 1 file changed, 134 insertions(+), 110 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 7a3aef40..82b2f624 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -1,5 +1,5 @@ import { Client } from "https://deno.land/x/mysql@v2.12.1/mod.ts"; -import { Query, replaceParams } from "https://deno.land/x/sql_builder@v1.9.2/mod.ts"; +import { Query } from "https://deno.land/x/sql_builder@v1.9.2/mod.ts"; import { Where } from "https://deno.land/x/sql_builder@v1.9.2/where.ts"; import { Pointer } from "../../runtime/pointers.ts"; import { AsyncStorageLocation } from "../storage.ts"; @@ -12,6 +12,7 @@ import { client_type } from "../../utils/constants.ts"; import { Compiler } from "../../compiler/compiler.ts"; import { ExecConditions } from "../../utils/global_types.ts"; import { Runtime } from "../../runtime/runtime.ts"; +import { Storage } from "../storage.ts"; const logger = new Logger("SQL Storage"); @@ -29,28 +30,28 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { readonly #metaTables = { typeMapping: { - name: "datex_types", + name: "__datex_types", columns: [ ["type", "varchar(50)", "PRIMARY KEY"], ["table_name", "varchar(50)"] ] }, pointerMapping: { - name: "datex_pointer_mapping", + name: "__datex_pointer_mapping", columns: [ [this.#pointerMysqlColumnName, this.#pointerMysqlType, "PRIMARY KEY"], ["table_name", "varchar(50)"] ] }, rawPointers: { - name: "datex_pointers_raw", + name: "__datex_pointers_raw", columns: [ [this.#pointerMysqlColumnName, "varchar(50)", "PRIMARY KEY"], ["value", "blob"] ] }, items: { - name: "datex_items", + name: "__datex_items", columns: [ ["key", "varchar(200)", "PRIMARY KEY"], ["value", "blob"] @@ -163,6 +164,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #getTableForType(type: Datex.Type) { + + // type does not have a template, use raw pointer table + if (!type.template) return null + const existingTable = (await this.#queryFirst<{table_name: string}|undefined>( new Query() .table(this.#metaTables.typeMapping.name) @@ -220,7 +225,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } // no matching primitive type found else if (!mysqlType) { - let foreignTable = propType.template ? await this.#getTableForType(propType) : null; + let foreignTable = await this.#getTableForType(propType); if (!foreignTable) { logger.warn("Cannot map type " + propType + " to a SQL table, falling back to raw DXB storage") @@ -293,8 +298,12 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return this.#tableColumns.get(tableName)!; } + /** + * Insert a pointer into the database, pointer type must be templated + */ async #insertPointer(pointer: Datex.Pointer) { const table = await this.#getTableForType(pointer.type) + if (!table) throw new Error("Cannot store pointer of type " + pointer.type + " in a custom table") const columns = await this.#getTableColumns(table); const insertData:Record = { @@ -306,7 +315,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const propPointer = Datex.Pointer.getByValue(pointer.val[name]); if (!propPointer) throw new Error("Cannot reference non-pointer value in SQL table") insertData[name] = propPointer.id - await this.#insertPointer(propPointer) + await this.setPointer(propPointer, NOT_EXISTING) } else insertData[name] = pointer.val[name]; } @@ -318,8 +327,12 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { await this.#updatePointerMapping(pointer.id, table) } + /** + * Update a pointer in the database, pointer type must be templated + */ async #updatePointer(pointer: Datex.Pointer, keys:string[]) { - const table = await this.#getTableForType(pointer.type) + const table = await this.#getTableForType(pointer.type); + if (!table) throw new Error("Cannot store pointer of type " + pointer.type + " in a custom table") const columns = await this.#getTableColumns(table); for (const key of keys) { @@ -328,9 +341,13 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } + /** + * Check if a pointer entry exists in the database + */ async #pointerEntryExists(pointer: Datex.Pointer) { const table = await this.#getTableForType(pointer.type) - + // TODO: do we need to check if the pointer is actually in the table - if there + // is a table mapping entry, the pointer should be in the table const exists = await this.#queryFirst<{COUNT:number}>( `SELECT COUNT(*) as COUNT FROM ?? WHERE ??=?`, [ table, @@ -341,6 +358,100 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return (!!exists) && exists.COUNT > 0; } + async #getTemplatedPointerValueString(pointerId: string, table?: string) { + table = table ?? await this.#getPointerTable(pointerId); + if (!table) { + logger.error("No table found for pointer " + pointerId); + return null; + } + + const type = await this.#getTypeForTable(table); + if (!type) { + logger.error("No type found for table " + table); + return null; + } + + const object = await this.#getTemplatedPointerObject(pointerId, table); + if (!object) return null; + + // resolve foreign pointers + const foreignPointerPlaceholders: string[] = [] + // const foreignPointerPlaceholderPromises: Promise[] = [] + for (const [colName, {foreignPtr}] of this.#tableColumns.get(table)!.entries()) { + // is an object type with a template + if (foreignPtr) { + if (typeof object[colName] == "string") { + const ptrId = object[colName] as string + object[colName] = `\u0001${foreignPointerPlaceholders.length}` + foreignPointerPlaceholders.push("$"+ptrId) + } + else { + logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) + } + } + } + + // const foreignPointerPlaceholders = await Promise.all(foreignPointerPlaceholderPromises) + + const objectString = Datex.Runtime.valueToDatexStringExperimental(object, false, false) + .replace(/"\u0001(\d+)"/g, (_, index) => foreignPointerPlaceholders[parseInt(index)]??"void") + + return `${type.toString()} ${objectString}` + } + + async #getTemplatedPointerObject(pointerId: string, table?: string) { + table = table ?? await this.#getPointerTable(pointerId); + if (!table) { + logger.error("No table found for pointer " + pointerId); + return null; + } + const object = await this.#queryFirst>( + new Query() + .table(table) + .select("*") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .build() + ) + if (!object) return null; + const type = await this.#getTypeForTable(table); + if (!type) { + logger.error("No type found for table " + table); + return null; + } + return object; + } + + async #getTemplatedPointerValueDXB(pointerId: string, table?: string) { + const string = await this.#getTemplatedPointerValueString(pointerId, table); + if (!string) return null; + console.log("string: " + string) + const compiled = await Compiler.compile(string, [], {sign: false, encrypt: false, to: Datex.Runtime.endpoint}, false) as ArrayBuffer; + return compiled + } + + async #getPointerTable(pointerId: string) { + return (await this.#queryFirst<{table_name:string}>( + new Query() + .table(this.#metaTables.pointerMapping.name) + .select("table_name") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .build() + ))?.table_name; + } + + async #setPointerRaw(pointer: Pointer) { + console.log("storing raw pointer: " + Runtime.valueToDatexStringExperimental(pointer, true, true)) + const dependencies = new Set() + const encoded = Compiler.encodeValue(pointer, dependencies, true, false, true); + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointer.id, encoded], encoded]) + // add to pointer mapping + await this.#updatePointerMapping(pointer.id, this.#metaTables.rawPointers.name) + return dependencies; + } + + async #updatePointerMapping(pointerId: string, tableName: string) { + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE table_name=?;', [this.#metaTables.pointerMapping.name, [this.#pointerMysqlColumnName, "table_name"], [pointerId, tableName], tableName]) + } isSupported() { return client_type === "deno"; @@ -440,29 +551,14 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - // no template, just add a raw DXB entry + // no template, just add a raw DXB entry, partial updates are not supported else { - await this.setPointerRaw(pointer) + await this.#setPointerRaw(pointer) } return dependencies; } - async setPointerRaw(pointer: Pointer) { - console.log("storing raw pointer: " + Runtime.valueToDatexStringExperimental(pointer, true, true)) - const dependencies = new Set() - const encoded = Compiler.encodeValue(pointer, dependencies, true, false, true); - await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointer.id, encoded], encoded]) - // add to pointer mapping - await this.#updatePointerMapping(pointer.id, this.#metaTables.rawPointers.name) - // await datex_item_storage.setItem(key, Compiler.encodeValue(value, dependencies)); - return dependencies; - } - - async #updatePointerMapping(pointerId: string, tableName: string) { - await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE table_name=?;', [this.#metaTables.pointerMapping.name, [this.#pointerMysqlColumnName, "table_name"], [pointerId, tableName], tableName]) - } - async getPointerValue(pointerId: string, outer_serialized: boolean): Promise { // get table where pointer is stored const table = await this.#getPointerTable(pointerId); @@ -499,7 +595,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // is an object type with a template if (foreignPtr) { if (typeof object[colName] == "string") { - object[colName] = await this.getPointerValue(object[colName] as string, false); + object[colName] = await Storage.getPointer(object[colName] as string); } else { logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) @@ -511,76 +607,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - async #getTemplatedPointerValueString(pointerId: string, table?: string) { - table = table ?? await this.#getPointerTable(pointerId); - if (!table) { - logger.error("No table found for pointer " + pointerId); - return null; - } - - const type = await this.#getTypeForTable(table); - if (!type) { - logger.error("No type found for table " + table); - return null; - } - - const object = await this.#getTemplatedPointerObject(pointerId, table); - if (!object) return null; - - // resolve foreign pointers - const foreignPointerPlaceholders: string[] = [] - // const foreignPointerPlaceholderPromises: Promise[] = [] - for (const [colName, {foreignPtr}] of this.#tableColumns.get(table)!.entries()) { - // is an object type with a template - if (foreignPtr) { - if (typeof object[colName] == "string") { - const ptrId = object[colName] as string - object[colName] = `\u0001${foreignPointerPlaceholders.length}` - foreignPointerPlaceholders.push("$"+ptrId) - } - else { - logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) - } - } - } - - // const foreignPointerPlaceholders = await Promise.all(foreignPointerPlaceholderPromises) - - const objectString = Datex.Runtime.valueToDatexStringExperimental(object, false, false) - .replace(/"\u0001(\d+)"/g, (_, index) => foreignPointerPlaceholders[parseInt(index)]??"void") - - return `${type.toString()} ${objectString}` - } - - async #getTemplatedPointerObject(pointerId: string, table?: string) { - table = table ?? await this.#getPointerTable(pointerId); - if (!table) { - logger.error("No table found for pointer " + pointerId); - return null; - } - const object = await this.#queryFirst>( - new Query() - .table(table) - .select("*") - .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) - .build() - ) - if (!object) return null; - const type = await this.#getTypeForTable(table); - if (!type) { - logger.error("No type found for table " + table); - return null; - } - return object; - } - - async #getTemplatedPointerValueDXB(pointerId: string, table?: string) { - const string = await this.#getTemplatedPointerValueString(pointerId, table); - if (!string) return null; - console.log("string: " + string) - const compiled = await Compiler.compile(string, [], {sign: false, encrypt: false, to: Datex.Runtime.endpoint}, false) as ArrayBuffer; - return compiled - } + async removePointer(pointerId: string): Promise { // get table where pointer is stored @@ -616,20 +643,17 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } - async #getPointerTable(pointerId: string) { - return (await this.#queryFirst<{table_name:string}>( - new Query() - .table(this.#metaTables.pointerMapping.name) - .select("table_name") - .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) - .build() - ))?.table_name; - } - async setPointerValueDXB(pointerId: string, value: ArrayBuffer) { - await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointerId, value], value]) - // add to pointer mapping - await this.#updatePointerMapping(pointerId, this.#metaTables.rawPointers.name) + // check if raw pointer, otherwise not yet supported + const table = await this.#getPointerTable(pointerId); + if (table == this.#metaTables.rawPointers.name) { + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [table, [this.#pointerMysqlColumnName, "value"], [pointerId, value], value]) + // add to pointer mapping + await this.#updatePointerMapping(pointerId, this.#metaTables.rawPointers.name) + } + else { + logger.error("Setting raw dxb value for templated pointer is not yet supported in SQL storage (pointer: " + pointerId + ", table: " + table + ")"); + } } async hasPointer(pointerId: string): Promise { From b296cdb061068264bb5d54e99a277ffb6125f598 Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 14 Feb 2024 01:49:51 +0100 Subject: [PATCH 08/56] sql db basics working, nested templates still have some issues --- storage/storage-locations/sql-db.ts | 86 ++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 82b2f624..f38579ae 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -83,6 +83,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #resetAll() { + // drop all custom type tables const tables = await this.#query<{table_name:string}>( new Query() @@ -90,13 +91,26 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .select("table_name") .build() ) - const tableNames = tables.map(({table_name})=>'`'+table_name+'`') - await this.#query<{table_name:string}>(`DROP TABLE IF EXISTS ${tableNames.join(',')};`) + const tableNames = tables.map(({table_name})=>'`'+table_name+'`'); + + // TODO: better solution to handle drop with foreign constraints + // currently just runs multiple drop table queries on failure, which is not ideal + const iterations = 10; + for (let i = 0; i < iterations; i++) { + try { + await this.#query<{table_name:string}>(`DROP TABLE IF EXISTS ${tableNames.join(',')};`) + break; + } + catch (e) { + console.error("Failed to drop some tables due to foreign constraints, repeating", e) + } + } // truncate meta tables for (const table of Object.values(this.#metaTables)) { await this.#query<{table_name:string}>(`TRUNCATE TABLE ${table.name};`) } + } async #query(query_string:string, query_params?:any[]): Promise { @@ -116,7 +130,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - console.log("QUERY: " + query_string, query_params) + console.debug("QUERY: " + query_string, query_params) if (typeof query_string != "string") {console.error("invalid query:", query_string); throw new Error("invalid query")} if (!query_string) throw new Error("empty query"); @@ -163,6 +177,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { ).join(', ')}${definition.constraints?.length ? ',' + definition.constraints.join(',') : ''});`, [definition.name]) } + /** + * Returns the table name for a given type, creates a new table if it does not exist + * @param type + * @returns + */ async #getTableForType(type: Datex.Type) { // type does not have a template, use raw pointer table @@ -193,6 +212,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return Datex.Type.get(type.type) } + /** + * Returns the table name for a given type. + * Does not validate if the table exists + */ #typeToTableName(type: Datex.Type) { return type.namespace=="ext" ? type.name : `${type.namespace}_${type.name}`; } @@ -201,6 +224,15 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return type.namespace + ":" + type.name; } + *#iterateTableColumns(type: Datex.Type) { + const table = this.#typeToTableName(type); + const columns = this.#tableColumns.get(table); + if (!columns) throw new Error("Table columns for type " + type + " are not loaded"); + for (const data of columns) { + yield data + } + } + async #createTableForType(type: Datex.Type) { const columns:ColumnDefinition[] = [ [this.#pointerMysqlColumnName, this.#pointerMysqlType, 'PRIMARY KEY INVISIBLE DEFAULT "0"'] @@ -208,7 +240,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const constraints: ConstraintsDefinition[] = [] this.log?.("Creating table for type " + type) - console.log(type) for (const [propName, propType] of Object.entries(type.template as {[key:string]:Datex.Type})) { let mysqlType: mysql_data_type|undefined @@ -306,6 +337,8 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (!table) throw new Error("Cannot store pointer of type " + pointer.type + " in a custom table") const columns = await this.#getTableColumns(table); + const dependencies = new Set() + const insertData:Record = { [this.#pointerMysqlColumnName]: pointer.id } @@ -315,7 +348,9 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const propPointer = Datex.Pointer.getByValue(pointer.val[name]); if (!propPointer) throw new Error("Cannot reference non-pointer value in SQL table") insertData[name] = propPointer.id - await this.setPointer(propPointer, NOT_EXISTING) + // must immediately add entry for foreign constraint to work + await Storage.setPointer(propPointer, true) + dependencies.add(propPointer) } else insertData[name] = pointer.val[name]; } @@ -325,6 +360,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // add to pointer mapping await this.#updatePointerMapping(pointer.id, table) + return dependencies; } /** @@ -424,7 +460,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async #getTemplatedPointerValueDXB(pointerId: string, table?: string) { const string = await this.#getTemplatedPointerValueString(pointerId, table); if (!string) return null; - console.log("string: " + string) const compiled = await Compiler.compile(string, [], {sign: false, encrypt: false, to: Datex.Runtime.endpoint}, false) as ArrayBuffer; return compiled } @@ -440,7 +475,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #setPointerRaw(pointer: Pointer) { - console.log("storing raw pointer: " + Runtime.valueToDatexStringExperimental(pointer, true, true)) const dependencies = new Set() const encoded = Compiler.encodeValue(pointer, dependencies, true, false, true); await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointer.id, encoded], encoded]) @@ -460,7 +494,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async setItem(key: string,value: unknown) { const dependencies = new Set() const encoded = Compiler.encodeValue(value, dependencies); - console.log("db set item", key) await this.setItemValueDXB(key, encoded) return dependencies; } @@ -529,40 +562,43 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async setPointer(pointer: Pointer, partialUpdateKey: unknown|typeof NOT_EXISTING): Promise>> { - const dependencies = new Set() // is templatable pointer type if (pointer.type.template) { - this.log?.("update " + pointer.id + " - " + pointer.type, partialUpdateKey, await this.#pointerEntryExists(pointer)) + // this.log?.("update " + pointer.id + " - " + pointer.type, partialUpdateKey, await this.#pointerEntryExists(pointer)) // new full insert - if (!await this.#pointerEntryExists(pointer)) - await this.#insertPointer(pointer) + if (partialUpdateKey === NOT_EXISTING || !await this.#pointerEntryExists(pointer)) { + return this.#insertPointer(pointer) + } + // partial update else { - // partial update - if (partialUpdateKey !== NOT_EXISTING) { - if (typeof partialUpdateKey !== "string") throw new Error("invalid key type for SQL table: " + Datex.Type.ofValue(partialUpdateKey)) - await this.#updatePointer(pointer, [partialUpdateKey]) - } - // full update - else { - // TODO + if (typeof partialUpdateKey !== "string") throw new Error("invalid key type for SQL table: " + Datex.Type.ofValue(partialUpdateKey)) + await this.#updatePointer(pointer, [partialUpdateKey]) + const dependencies = new Set() + // add all pointer properties to dependencies + for (const [name, {foreignPtr}] of this.#iterateTableColumns(pointer.type)) { + if (foreignPtr) { + const ptr = Pointer.pointerifyValue(pointer.getProperty(name)); + if (ptr instanceof Pointer) { + dependencies.add(ptr) + } + } } + return dependencies; } } // no template, just add a raw DXB entry, partial updates are not supported else { - await this.#setPointerRaw(pointer) + return this.#setPointerRaw(pointer) } - return dependencies; } async getPointerValue(pointerId: string, outer_serialized: boolean): Promise { // get table where pointer is stored const table = await this.#getPointerTable(pointerId); - console.log("table for pointer", pointerId, table) if (!table) { logger.error("No table found for pointer " + pointerId); return null; @@ -595,14 +631,13 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // is an object type with a template if (foreignPtr) { if (typeof object[colName] == "string") { - object[colName] = await Storage.getPointer(object[colName] as string); + object[colName] = await Storage.getPointer(object[colName] as string, true); } else { logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) } } } - console.log("Templateo",object) return type.cast(object, undefined, undefined, false); } } @@ -622,7 +657,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async getPointerValueDXB(pointerId: string): Promise { // get table where pointer is stored const table = await this.#getPointerTable(pointerId); - console.log("table for pointer", pointerId, table) // is raw pointer if (table == this.#metaTables.rawPointers.name) { From 9723f64c4f8fd9871271de31a69c8f3294fdd587 Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 14 Feb 2024 02:58:32 +0100 Subject: [PATCH 09/56] sql db fixes, still very slow --- storage/storage-locations/sql-db.ts | 119 +++++++++++++++++----- storage/storage-locations/sql-type-map.ts | 2 + 2 files changed, 98 insertions(+), 23 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index f38579ae..52377308 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -13,6 +13,9 @@ import { Compiler } from "../../compiler/compiler.ts"; import { ExecConditions } from "../../utils/global_types.ts"; import { Runtime } from "../../runtime/runtime.ts"; import { Storage } from "../storage.ts"; +import { Type } from "../../types/type.ts"; +import { TypedArray } from "../../utils/global_values.ts"; +import { MessageLogger } from "../../utils/message_logger.ts"; const logger = new Logger("SQL Storage"); @@ -60,7 +63,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } satisfies Record; // cached table columns - #tableColumns = new Map>() + #tableColumns = new Map>() constructor(options:dbOptions, private log?:(...args:unknown[])=>void) { super() @@ -256,15 +259,24 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } // no matching primitive type found else if (!mysqlType) { - let foreignTable = await this.#getTableForType(propType); - if (!foreignTable) { - logger.warn("Cannot map type " + propType + " to a SQL table, falling back to raw DXB storage") - foreignTable = this.#metaTables.rawPointers.name; + // is a primitive type -> assume no pointer, just store as dxb inline + if (propType == Type.std.Any || propType.is_primitive || propType.is_js_pseudo_primitive ) { + logger.warn("Cannot map primitive type " + propType + " to a SQL table, falling back to raw DXB") + columns.push([propName, "blob"]) + } + else { + let foreignTable = await this.#getTableForType(propType); + + if (!foreignTable) { + logger.warn("Cannot map type " + propType + " to a SQL table, falling back to raw pointer storage") + foreignTable = this.#metaTables.rawPointers.name; + } + + columns.push([propName, this.#pointerMysqlType]) + constraints.push(`FOREIGN KEY (\`${propName}\`) REFERENCES \`${foreignTable}\`(\`${this.#pointerMysqlColumnName}\`)`) } - columns.push([propName, this.#pointerMysqlType]) - constraints.push(`FOREIGN KEY (\`${propName}\`) REFERENCES \`${foreignTable}\`(\`${this.#pointerMysqlColumnName}\`)`) } else { @@ -309,11 +321,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async #getTableColumns(tableName: string) { if (!this.#tableColumns.has(tableName)) { - const columnData = new Map() - const columns = await this.#query<{COLUMN_NAME:string, COLUMN_KEY:string}>( + const columnData = new Map() + const columns = await this.#query<{COLUMN_NAME:string, COLUMN_KEY:string, DATA_TYPE:string}>( new Query() .table("information_schema.columns") - .select("COLUMN_NAME", "COLUMN_KEY") + .select("COLUMN_NAME", "COLUMN_KEY", "DATA_TYPE") .where(Where.eq("table_schema", this.#options.db)) .where(Where.eq("table_name", tableName)) .build() @@ -321,7 +333,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { for (const col of columns) { if (col.COLUMN_NAME == this.#pointerMysqlColumnName) continue; - columnData.set(col.COLUMN_NAME, {foreignPtr: col.COLUMN_KEY == "MUL"}) + columnData.set(col.COLUMN_NAME, {foreignPtr: col.COLUMN_KEY == "MUL", type: col.DATA_TYPE}) } this.#tableColumns.set(tableName, columnData) @@ -343,16 +355,29 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { [this.#pointerMysqlColumnName]: pointer.id } - for (const [name, {foreignPtr}] of columns) { + for (const [name, {foreignPtr, type}] of columns) { + const value = pointer.val[name]; if (foreignPtr) { - const propPointer = Datex.Pointer.getByValue(pointer.val[name]); - if (!propPointer) throw new Error("Cannot reference non-pointer value in SQL table") - insertData[name] = propPointer.id - // must immediately add entry for foreign constraint to work - await Storage.setPointer(propPointer, true) - dependencies.add(propPointer) + const propPointer = Datex.Pointer.getByValue(value); + // no pointer value + if (!propPointer) { + // null values are okay, otherwise error + if (value !== undefined) { + logger.error("Cannot reference non-pointer value in SQL table") + } + } + else { + insertData[name] = propPointer.id + // must immediately add entry for foreign constraint to work + await Storage.setPointer(propPointer, true) + dependencies.add(propPointer) + } } - else insertData[name] = pointer.val[name]; + // is raw dxb value (exception for blob <->A rrayBuffer, TODO: better solution, can lead to issues) + else if (type == "blob" && !(value instanceof ArrayBuffer || value instanceof TypedArray)) { + insertData[name] = Compiler.encodeValue(value, dependencies, true, false, true); + } + else insertData[name] = value; } // this.log("cols", insertData) @@ -372,7 +397,18 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const columns = await this.#getTableColumns(table); for (const key of keys) { - const val = columns.get(key)?.foreignPtr ? Datex.Pointer.getByValue(pointer.val[key])!.id : pointer.val[key]; + const column = columns.get(key); + const val = + column?.foreignPtr ? + // foreign pointer id + Datex.Pointer.getByValue(pointer.val[key])!.id : + ( + (column?.type == "blob" && !(pointer.val[key] instanceof ArrayBuffer || pointer.val[key] instanceof TypedArray)) ? + // raw dxb value + Compiler.encodeValue(pointer.val[key], new Set(), true, false, true) : + // normal value + pointer.val[key] + ) await this.#query('UPDATE ?? SET ?? = ? WHERE ?? = ?;', [table, key, val, this.#pointerMysqlColumnName, pointer.id]) } } @@ -413,7 +449,15 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // resolve foreign pointers const foreignPointerPlaceholders: string[] = [] // const foreignPointerPlaceholderPromises: Promise[] = [] - for (const [colName, {foreignPtr}] of this.#tableColumns.get(table)!.entries()) { + const columns = await this.#getTableColumns(table); + if (!columns) throw new Error("No columns found for table " + table) + for (const [colName, {foreignPtr, type}] of columns.entries()) { + + // convert blob strings to ArrayBuffer + if (type == "blob" && typeof object[colName] == "string") { + object[colName] = this.#stringToBinary(object[colName] as string) + } + // is an object type with a template if (foreignPtr) { if (typeof object[colName] == "string") { @@ -425,12 +469,29 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) } } + // is blob, assume it is a DXB value + else if (type == "blob") { + object[colName] = `\u0001${foreignPointerPlaceholders.length}` + try { + // TODO: fix decompiling + foreignPointerPlaceholders.push(MessageLogger.decompile(object[colName] as ArrayBuffer, false, false, false)||"'error: empty'") + } + catch (e) { + console.error("error decompiling", object[colName], e) + foreignPointerPlaceholders.push("'error'") + } + + } } // const foreignPointerPlaceholders = await Promise.all(foreignPointerPlaceholderPromises) + console.log("foreignPointerPlaceholders", foreignPointerPlaceholders) + const objectString = Datex.Runtime.valueToDatexStringExperimental(object, false, false) - .replace(/"\u0001(\d+)"/g, (_, index) => foreignPointerPlaceholders[parseInt(index)]??"void") + .replace(/"\u0001(\d+)"/g, (_, index) => foreignPointerPlaceholders[parseInt(index)]||"error: no placeholder") + + console.log("objectString", objectString) return `${type.toString()} ${objectString}` } @@ -627,7 +688,15 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (!object) return null; // resolve foreign pointers - for (const [colName, {foreignPtr}] of this.#tableColumns.get(table)!.entries()) { + const columns = await this.#getTableColumns(table); + if (!columns) throw new Error("No columns found for table " + table) + for (const [colName, {foreignPtr, type}] of columns.entries()) { + + // convert blob strings to ArrayBuffer + if (type == "blob" && typeof object[colName] == "string") { + object[colName] = this.#stringToBinary(object[colName] as string) + } + // is an object type with a template if (foreignPtr) { if (typeof object[colName] == "string") { @@ -637,6 +706,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) } } + // is blob, assume it is a DXB value + else if (type == "blob") { + object[colName] = await Runtime.decodeValue(object[colName] as ArrayBuffer, true); + } } return type.cast(object, undefined, undefined, false); } diff --git a/storage/storage-locations/sql-type-map.ts b/storage/storage-locations/sql-type-map.ts index 390c6607..48253f88 100644 --- a/storage/storage-locations/sql-type-map.ts +++ b/storage/storage-locations/sql-type-map.ts @@ -56,6 +56,8 @@ export const datex_type_mysql_map = new Map([ [Datex.Type.std.text, 'text'], + [Datex.Type.std.boolean, 'boolean'], + // ['smallint', Datex.Type.std.integer], // ['mediumint', Datex.Type.std.integer], // ['tinyint', Datex.Type.std.integer], From 5ebabb0742b05d04d3fd2ef7c510111a0949d688 Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 20 Feb 2024 16:38:44 +0100 Subject: [PATCH 10/56] custom storage collection timeout/pointer setting, auto proxify children --- runtime/pointers.ts | 2 +- storage/storage.ts | 102 +++++++++++++++++++++++++++++++++++++++---- types/storage-map.ts | 30 ++++++++++--- types/storage-set.ts | 27 +++++++++++- 4 files changed, 144 insertions(+), 17 deletions(-) diff --git a/runtime/pointers.ts b/runtime/pointers.ts index a35bf19f..eb65384f 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -3460,7 +3460,7 @@ export class Pointer extends Ref { } // proxify a (child) value, use the pointer context - private proxifyChild(name:string, value:unknown) { + proxifyChild(name:string, value:unknown) { if (NOT_EXISTING && !this.shadow_object) throw new Error("Cannot proxify child of non-object value"); let child = value === NOT_EXISTING ? this.shadow_object![name] : value; diff --git a/storage/storage.ts b/storage/storage.ts index b0c626ea..e59a87c4 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -17,6 +17,7 @@ import { StorageMap } from "../types/storage-map.ts"; import { StorageSet } from "../types/storage-set.ts"; import { IterableWeakSet } from "../utils/iterable-weak-set.ts"; import { LazyPointer } from "../runtime/lazy-pointer.ts"; +import { AutoMap } from "../utils/auto_map.ts"; // displayInit(); @@ -129,8 +130,18 @@ type storage_location_options = L extends StorageLocation ? storage_options : never type StorageSnapshotOptions = { + /** + * Display all internally used items (e.g. for garbage collection) + */ internalItems: boolean, - expandStorageMapsAndSets: boolean + /** + * List all items and pointers of storage maps and sets + */ + expandStorageMapsAndSets: boolean, + /** + * Only display items (and related pointers) that contain the given string in their key + */ + itemFilter?: string } export class Storage { @@ -1289,9 +1300,15 @@ export class Storage { } + private static removeTrailingSemicolon(str:string) { + // replace ; and reset sequences with nothing + return str.replace(/;\x1b\[0m$/g, "") + } + public static async getSnapshot(options: StorageSnapshotOptions = {internalItems: false, expandStorageMapsAndSets: true}) { - const items = await this.createSnapshot(this.getItemKeys.bind(this), this.getItemDecompiled.bind(this)); - const pointers = await this.createSnapshot(this.getPointerIds.bind(this), this.getPointerDecompiledFromLocation.bind(this)); + const allowedPointerIds = options.itemFilter ? new Set() : undefined; + const items = await this.createSnapshot(this.getItemKeys.bind(this), this.getItemDecompiled.bind(this), options.itemFilter, allowedPointerIds); + const pointers = await this.createSnapshot(this.getPointerIds.bind(this), this.getPointerDecompiledFromLocation.bind(this), options.itemFilter, allowedPointerIds); // remove keys items that are unrelated to normal storage for (const [key] of items.snapshot) { @@ -1305,6 +1322,9 @@ export class Storage { } } + // additional pointer entries from storage maps/sets + const additionalEntries = new Set(); + // iterate over storage maps and sets and render all entries if (options.expandStorageMapsAndSets) { for (const [ptrId, storageMap] of pointers.snapshot) { @@ -1327,7 +1347,21 @@ export class Storage { logger.error("Invalid entry in storage (" + location.name + "): " + key); continue; } - inner += ` ${Runtime.valueToDatexStringExperimental(key, true, true)}\x1b[0m => ${valString}\n` + const keyString = await this.getItemDecompiled('key.' + key, true, location); + if (keyString === NOT_EXISTING) { + logger.error("Invalid key in storage (" + location.name + "): " + key); + continue; + } + inner += ` ${this.removeTrailingSemicolon(keyString)}\x1b[0m => ${this.removeTrailingSemicolon(valString)}\n` + + // additional pointer ids included in value or key + if (allowedPointerIds) { + const matches = [...valString.match(/\$[a-zA-Z0-9]+/g)??[], ...keyString.match(/\$[a-zA-Z0-9]+/g)??[]]; + for (const match of matches) { + const id = match.substring(1); + if (!allowedPointerIds.has(id)) additionalEntries.add(id); + } + } } // substring: remove last \n if (inner) storageMap.set(location, "\x1b[38;2;50;153;220m \x1b[0m{\n"+inner.substring(0, inner.length-1)+"\x1b[0m\n}") @@ -1342,7 +1376,16 @@ export class Storage { logger.error("Invalid entry in storage (" + location.name + "): " + key); continue; } - inner += ` ${valString},\n` + inner += ` ${this.removeTrailingSemicolon(valString)},\n` + + // additional pointer ids included in value + if (allowedPointerIds) { + const matches = valString.match(/\$[a-zA-Z0-9]+/g)??[]; + for (const match of matches) { + const id = match.substring(1); + if (!allowedPointerIds.has(id)) additionalEntries.add(id); + } + } } // substring: remove last \n if (inner) storageMap.set(location, "\x1b[38;2;50;153;220m \x1b[0m{\n"+inner.substring(0, inner.length-1)+"\x1b[0m\n}") @@ -1351,26 +1394,67 @@ export class Storage { } } + if (additionalEntries.size > 0) { + await this.createSnapshot(this.getPointerIds.bind(this), this.getPointerDecompiledFromLocation.bind(this), options.itemFilter, additionalEntries, { + snapshot: pointers.snapshot, + inconsistencies: pointers.inconsistencies + }); + } + return {items, pointers} } private static async createSnapshot( keyGenerator: (location?: StorageLocation | undefined) => Promise>, itemGetter: (key: string, colorized: boolean, location: StorageLocation) => Promise, + filter?: string, + allowedPointerIds?: Set, + baseSnapshot?: { + snapshot: AutoMap, string>>; + inconsistencies: AutoMap, string>>; + } ) { - const snapshot = new Map>().setAutoDefault(Map); - const inconsistencies = new Map>().setAutoDefault(Map); + const snapshot = baseSnapshot?.snapshot ?? new Map>().setAutoDefault(Map); + const inconsistencies = baseSnapshot?.inconsistencies ?? new Map>().setAutoDefault(Map); + + const skippedEntries = new Set(); + const additionalEntries = new Set(); + for (const location of new Set([this.#primary_location!, ...this.#locations.keys()].filter(l=>!!l))) { for (const key of await keyGenerator(location)) { + if (filter && !key.includes(filter) && !allowedPointerIds?.has(key)) { + if (allowedPointerIds) skippedEntries.add(key); // remember skipped entries that might be added later + continue; + } const decompiled = await itemGetter(key, true, location); + if (typeof decompiled !== "string") { console.error("Invalid entry in storage (" + location.name + "): " + key); continue; } - snapshot.getAuto(key).set(location, decompiled); + + // collect referenced pointer ids + if (allowedPointerIds) { + const matches = decompiled.match(/\$[a-zA-Z0-9]+/g); + if (matches) { + for (const match of matches) { + const id = match.substring(1); + if (skippedEntries.has(id)) additionalEntries.add(id); + allowedPointerIds.add(id); + } + } + } + snapshot.getAuto(key).set(location, this.removeTrailingSemicolon(decompiled)); } } - + + // run again with additional entries + if (additionalEntries.size > 0) { + await this.createSnapshot(keyGenerator, itemGetter, filter, additionalEntries, { + snapshot, + inconsistencies + }); + } // find inconsistencies for (const [key, storageMap] of snapshot) { diff --git a/types/storage-map.ts b/types/storage-map.ts index dfd3ebfb..47c42051 100644 --- a/types/storage-map.ts +++ b/types/storage-map.ts @@ -4,9 +4,6 @@ import { Compiler } from "../compiler/compiler.ts"; import { DX_PTR } from "../runtime/constants.ts"; import { Pointer } from "../runtime/pointers.ts"; import { Storage } from "../storage/storage.ts"; -import { Logger } from "../utils/logger.ts"; - -const logger = new Logger("StorageMap"); /** @@ -20,10 +17,30 @@ export class StorageWeakMap { #prefix?: string; + /** + * Time in milliseconds after which a value is removed from the in-memory cache + * Default: 5min + */ + cacheTimeout = 5 * 60 * 1000; + + /** + * If true, non-pointer objects are allowed as + * values in the map (default) + * Otherwise, object values are automatically proxified + * when added to the map. + */ + allowNonPointerObjectValues = false; + + constructor(){ Pointer.proxifyValue(this) } + #_pointer?: Pointer; + get #pointer() { + if (!this.#_pointer) this.#_pointer = Pointer.getByValue(this); + return this.#_pointer; + } static async from(entries: readonly (readonly [K, V])[]){ const map = $$(new StorageWeakMap()); @@ -67,15 +84,18 @@ export class StorageWeakMap { return this._set(storage_key, value); } protected _set(storage_key:string, value:V) { + // proxify value + if (!this.allowNonPointerObjectValues) { + value = this.#pointer.proxifyChild("", value); + } this.activateCacheTimeout(storage_key); return Storage.setItem(storage_key, value) } protected activateCacheTimeout(storage_key:string){ setTimeout(()=>{ - logger.debug("removing item from cache: " + storage_key); Storage.cache.delete(storage_key) - }, 60_000); + }, this.cacheTimeout); } protected async getStorageKey(key: K) { diff --git a/types/storage-set.ts b/types/storage-set.ts index 1e4b0163..a67cfee0 100644 --- a/types/storage-set.ts +++ b/types/storage-set.ts @@ -17,8 +17,28 @@ const logger = new Logger("StorageSet"); */ export class StorageWeakSet { + /** + * Time in milliseconds after which a value is removed from the in-memory cache + * Default: 5min + */ + cacheTimeout = 5 * 60 * 1000; + + /** + * If true, non-pointer objects are allowed as + * values in the map (default) + * Otherwise, object values are automatically proxified + * when added to the map. + */ + allowNonPointerObjectValues = false; + #prefix?: string; + #_pointer?: Pointer; + get #pointer() { + if (!this.#_pointer) this.#_pointer = Pointer.getByValue(this); + return this.#_pointer; + } + constructor(){ Pointer.proxifyValue(this) } @@ -41,6 +61,10 @@ export class StorageWeakSet { return this; } protected _add(storage_key:string, value:V|null) { + // proxify value + if (!this.allowNonPointerObjectValues) { + value = this.#pointer.proxifyChild("", value); + } this.activateCacheTimeout(storage_key); return Storage.setItem(storage_key, value); } @@ -63,9 +87,8 @@ export class StorageWeakSet { protected activateCacheTimeout(storage_key:string){ setTimeout(()=>{ - logger.debug("removing item from cache: " + storage_key); Storage.cache.delete(storage_key) - }, 60_000); + }, this.cacheTimeout); } protected async getStorageKey(value: V) { From 5c4e253915e24ef45c26de6ea3f0199eed2aae96 Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 21 Feb 2024 00:11:23 +0100 Subject: [PATCH 11/56] add first match query implementation for storage/sql db --- runtime/pointers.ts | 27 ++- runtime/runtime.ts | 3 +- storage/storage-locations/sql-db.ts | 251 +++++++++++++++++++++------- storage/storage.ts | 67 +++++++- types/storage-map.ts | 2 + types/storage-set.ts | 23 +-- utils/match.ts | 160 ++++++++++++++++++ 7 files changed, 439 insertions(+), 94 deletions(-) create mode 100644 utils/match.ts diff --git a/runtime/pointers.ts b/runtime/pointers.ts index eb65384f..de7d63c0 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -29,8 +29,8 @@ import { IterableWeakMap } from "../utils/iterable-weak-map.ts"; import { LazyPointer } from "./lazy-pointer.ts"; import { ReactiveArrayMethods } from "../types/reactive-methods/array.ts"; import { Assertion } from "../types/assertion.ts"; -import { StorageSet } from "../types/storage-set.ts"; import { Storage } from "../storage/storage.ts"; +import { client_type } from "../utils/constants.ts"; export type observe_handler = (value:V extends RefLike ? T : V, key?:K, type?:Ref.UPDATE_TYPE, transform?:boolean, is_child_update?:boolean, previous?: any, atomic_id?:symbol)=>void|boolean export type observe_options = {types?:Ref.UPDATE_TYPE[], ignore_transforms?:boolean, recursive?:boolean} @@ -1285,6 +1285,18 @@ export class Pointer extends Ref { } public static get is_local() {return this.#is_local} + #createdInContext = true; + /** + * Indicates if the pointer was created in the current context + * or fetched (from storage or network) + */ + public get createdInContext() { + return this.#createdInContext; + } + public set createdInContext(fetched: boolean) { + this.#createdInContext = fetched; + } + /** 21 bytes address: 1 byte address type () 18/16 byte origin id - 2/4 byte origin instance - 4 bytes timestamp - 1 byte counter*/ /** * Endpoint id types: @@ -1462,6 +1474,9 @@ export class Pointer extends Ref { // get value if pointer value not yet loaded if (!pointer.#loaded) { + + // was not created new in current context + pointer.createdInContext = false; // first try loading from storage let stored:any = NOT_EXISTING; @@ -2710,7 +2725,8 @@ export class Pointer extends Ref { // potential storage pointer initialized Storage.providePointer(this); - if (this.isStored) { + // only in frontend, disabled for backend (TODO) + if (this.isStored && client_type == "browser") { // get subsriber caches Storage.getPointerSubscriberCache(this.id).then(cache => { if (cache) { @@ -3276,8 +3292,9 @@ export class Pointer extends Ref { // already subscribed if (this.subscribers.has(subscriber)) return; - // also store in subscriber cache - if (this.isStored) { + // also store in subscriber cache - only in frontend + // (TODO: required for backend? currently disabled because backend is not stopped frequently, only leads to overhead) + if (this.isStored && client_type == "browser") { if (this.#subscriberCache) this.#subscriberCache.add(subscriber) else { Storage.requestSubscriberCache(this.id).then(cache => { @@ -3461,7 +3478,7 @@ export class Pointer extends Ref { // proxify a (child) value, use the pointer context proxifyChild(name:string, value:unknown) { - if (NOT_EXISTING && !this.shadow_object) throw new Error("Cannot proxify child of non-object value"); + if (value === NOT_EXISTING && !this.shadow_object) throw new Error("Cannot proxify child of non-object value"); let child = value === NOT_EXISTING ? this.shadow_object![name] : value; // special native function -> conversion; diff --git a/runtime/runtime.ts b/runtime/runtime.ts index 4a68e775..fb200fee 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -7208,7 +7208,8 @@ RuntimePerformance.createMeasureGroup("compile time", [ // automatically sync newly added pointers if they are in the storage Pointer.onPointerAdded(async (pointer)=>{ - if (await Storage.hasPointer(pointer)) { + // assume that already synced if createdInContext and stored in storage + if (!pointer.createdInContext && await Storage.hasPointer(pointer)) { Storage.syncPointer(pointer); } }) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 52377308..55c2b19e 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -1,4 +1,4 @@ -import { Client } from "https://deno.land/x/mysql@v2.12.1/mod.ts"; +import { Client, ExecuteResult } from "https://deno.land/x/mysql@v2.12.1/mod.ts"; import { Query } from "https://deno.land/x/sql_builder@v1.9.2/mod.ts"; import { Where } from "https://deno.land/x/sql_builder@v1.9.2/where.ts"; import { Pointer } from "../../runtime/pointers.ts"; @@ -16,11 +16,14 @@ import { Storage } from "../storage.ts"; import { Type } from "../../types/type.ts"; import { TypedArray } from "../../utils/global_values.ts"; import { MessageLogger } from "../../utils/message_logger.ts"; +import { Join } from "https://deno.land/x/sql_builder@v1.9.2/join.ts"; const logger = new Logger("SQL Storage"); export class SQLDBStorageLocation extends AsyncStorageLocation { name = "SQL_DB" + supportsPrefixSelection = true; + supportsMatchSelection = true; #connected = false; #initializing = false @@ -57,13 +60,19 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { name: "__datex_items", columns: [ ["key", "varchar(200)", "PRIMARY KEY"], - ["value", "blob"] + ["value", "blob"], + [this.#pointerMysqlColumnName, this.#pointerMysqlType], ] } } satisfies Record; // cached table columns #tableColumns = new Map>() + // cached table -> type mapping + #tableTypes = new Map() + + #existingItemsCache = new Set() + #existingPointersCache = new Set() constructor(options:dbOptions, private log?:(...args:unknown[])=>void) { super() @@ -116,7 +125,9 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } - async #query(query_string:string, query_params?:any[]): Promise { + async #query(query_string:string, query_params?:any[], returnRawResult: true): Promise<{rows:row[], result:ExecuteResult}> + async #query(query_string:string, query_params?:any[]): Promise + async #query(query_string:string, query_params?:any[], returnRawResult?: boolean): Promise { // prevent infinite recursion if calling query from within init() if (!this.#initializing) await this.#init(); @@ -133,13 +144,14 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - console.debug("QUERY: " + query_string, query_params) + console.warn("QUERY: " + query_string, query_params) if (typeof query_string != "string") {console.error("invalid query:", query_string); throw new Error("invalid query")} if (!query_string) throw new Error("empty query"); try { const result = await this.#sqlClient!.execute(query_string, query_params); - return result.rows ?? []; + if (returnRawResult) return {rows: result.rows ?? [], result}; + else return result.rows ?? []; } catch (e){ if (this.log) this.log("SQL error:", e) else console.error("SQL error:", e); @@ -204,15 +216,21 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #getTypeForTable(table: string) { - const type = await this.#queryFirst<{type:string}>( - new Query() - .table(this.#metaTables.typeMapping.name) - .select("type") - .where(Where.eq("table_name", table)) - .build() - ) - if (!type) return null; - return Datex.Type.get(type.type) + if (!this.#tableTypes.has(table)) { + const type = await this.#queryFirst<{type:string}>( + new Query() + .table(this.#metaTables.typeMapping.name) + .select("type") + .where(Where.eq("table_name", table)) + .build() + ) + if (!type) { + logger.error("No type found for table " + table); + } + this.#tableTypes.set(table, type ? Datex.Type.get(type.type) : null) + } + + return this.#tableTypes.get(table) } /** @@ -413,23 +431,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - /** - * Check if a pointer entry exists in the database - */ - async #pointerEntryExists(pointer: Datex.Pointer) { - const table = await this.#getTableForType(pointer.type) - // TODO: do we need to check if the pointer is actually in the table - if there - // is a table mapping entry, the pointer should be in the table - const exists = await this.#queryFirst<{COUNT:number}>( - `SELECT COUNT(*) as COUNT FROM ?? WHERE ??=?`, [ - table, - this.#pointerMysqlColumnName, - pointer.id - ] - ); - return (!!exists) && exists.COUNT > 0; - } - async #getTemplatedPointerValueString(pointerId: string, table?: string) { table = table ?? await this.#getPointerTable(pointerId); if (!table) { @@ -538,9 +539,9 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async #setPointerRaw(pointer: Pointer) { const dependencies = new Set() const encoded = Compiler.encodeValue(pointer, dependencies, true, false, true); - await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointer.id, encoded], encoded]) + const {result} = await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointer.id, encoded], encoded], true) // add to pointer mapping - await this.#updatePointerMapping(pointer.id, this.#metaTables.rawPointers.name) + if (result.affectedRows == 1) await this.#updatePointerMapping(pointer.id, this.#metaTables.rawPointers.name) return dependencies; } @@ -548,23 +549,127 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE table_name=?;', [this.#metaTables.pointerMapping.name, [this.#pointerMysqlColumnName, "table_name"], [pointerId, tableName], tableName]) } + async #setItemPointer(key: string, pointer: Pointer) { + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE '+this.#pointerMysqlColumnName+'=?;', [this.#metaTables.items.name, ["key", this.#pointerMysqlColumnName], [key, pointer.id], pointer.id]) + } + isSupported() { return client_type === "deno"; } + async supportsMatchForType(type: Datex.Type): Promise { + return (await this.#getTableForType(type)) != null; + } + + async matchQuery(itemPrefix: string, valueType: Datex.Type, match: Datex.MatchInput, limit: number): Promise { + + const builder = new Query() + .table(this.#metaTables.items.name) + // .select(this.#pointerMysqlColumnName) + .select(`${this.#typeToTableName(valueType)}.${this.#pointerMysqlColumnName} as ptrId`) + .where(Where.like("key", itemPrefix + "%")) + .join( + Join.left(this.#typeToTableName(valueType)).on(`${this.#metaTables.items.name}.${this.#pointerMysqlColumnName}`, `${this.#typeToTableName(valueType)}.${this.#pointerMysqlColumnName}`) + ) + + if (isFinite(limit)) builder.limit(0, limit) + + const joins = new Map() + const where = this.buildQueryConditions(builder, match, joins, valueType) + + builder.where(where); + joins.forEach(join => builder.join(join)); + + const ptrIds = await this.#query<{ptrId:string}>(builder.build()) + return Promise.all(ptrIds.map(({ptrId}) => Storage.getPointer(ptrId))) + } + + private buildQueryConditions(builder: Query, match: unknown, joins: Map, valueType:Type, namespacedKey?: string, previousKey?: string): Where { + + const matchOrs = match instanceof Array ? match : [match] + const entryIdentifier = previousKey ? previousKey + '.' + namespacedKey : namespacedKey // address.street + + let isPrimitiveArray = true; + + for (const or of matchOrs) { + if (typeof or == "object") { + isPrimitiveArray = false; + break; + } + } + + + // only primitive array, use IN selector + if (isPrimitiveArray) { + if (!namespacedKey) throw new Error("missing namespacedKey"); + if (matchOrs.length == 1) return Where.eq(entryIdentifier!, matchOrs[0]) + else return Where.in(entryIdentifier!, matchOrs) + } + + else { + const wheresOr = [] + for (const or of matchOrs) { + + if (typeof or == "object") { + + // is pointer + const ptr = Pointer.pointerifyValue(or); + if (ptr instanceof Pointer) { + wheresOr.push(Where.eq(entryIdentifier!, ptr.id)) + continue; + } + + // only enter after first recursion + if (namespacedKey) { + const tableAName = this.#typeToTableName(valueType) + '.' + namespacedKey // User.address + const tableBName = this.#typeToTableName(valueType.template[namespacedKey]); // Address + const tableBIdentifier = namespacedKey + '.' + this.#pointerMysqlColumnName + // Join Adddreess on address._ptr_id = User.address + joins.set(namespacedKey, Join.left(`${tableBName}`, namespacedKey).on(tableBIdentifier, tableAName)); + valueType = valueType.template[namespacedKey]; + } + + const whereAnds = [] + for (const [key, value] of Object.entries(or)) { + // const nestedKey = namespacedKey ? namespacedKey + "." + key : key; + whereAnds.push(this.buildQueryConditions(builder, value, joins, valueType, key, namespacedKey)) + } + wheresOr.push(Where.and(...whereAnds)) + } + else { + if (!namespacedKey) throw new Error("missing namespacedKey"); + wheresOr.push(Where.eq(entryIdentifier!, or)) + } + } + return Where.or(...wheresOr) + } + + } + + async setItem(key: string,value: unknown) { const dependencies = new Set() - const encoded = Compiler.encodeValue(value, dependencies); - await this.setItemValueDXB(key, encoded) + + // value is pointer + const ptr = Pointer.pointerifyValue(value); + if (ptr instanceof Pointer) { + dependencies.add(ptr); + this.#setItemPointer(key, ptr) + } + else { + const encoded = Compiler.encodeValue(value, dependencies); + await this.setItemValueDXB(key, encoded) + } return dependencies; } async getItem(key: string, conditions: ExecConditions): Promise { const encoded = await this.getItemValueDXB(key); - if (!encoded) return null; + if (encoded === null) return NOT_EXISTING; return Runtime.decodeValue(encoded, false, conditions); } async hasItem(key:string) { + if (this.#existingItemsCache.has(key)) return true; const count = (await this.#queryFirst<{COUNT: number}>( new Query() .table(this.#metaTables.items.name) @@ -572,16 +677,23 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .where(Where.eq("key", key)) .build() )); - return (!!count) && count.COUNT > 0; + const exists = !!count && count.COUNT > 0; + if (exists) { + this.#existingItemsCache.add(key); + // delete from cache after 2 minutes + setTimeout(()=>this.#existingItemsCache.delete(key), 1000*60*2) + } + return exists; } - async getItemKeys() { - const keys = await this.#query<{key:string}>( - new Query() - .table(this.#metaTables.items.name) - .select("key") - .build() - ) + async getItemKeys(prefix: string) { + const builder = new Query() + .table(this.#metaTables.items.name) + .select("key") + + if (prefix != undefined) builder.where(Where.like("key", prefix + "%")) + + const keys = await this.#query<{key:string}>(builder.build()) return function*(){ for (const {key} of keys) { yield key; @@ -590,32 +702,34 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async getPointerIds() { - const pointerIds = await this.#query<{_ptr_id:string}>( + const pointerIds = await this.#query<{ptrId:string}>( new Query() .table(this.#metaTables.pointerMapping.name) - .select(this.#pointerMysqlColumnName) + .select(`${this.#pointerMysqlColumnName} as ptrId`) .build() ) return function*(){ - for (const {_ptr_id} of pointerIds) { - yield _ptr_id; + for (const {ptrId} of pointerIds) { + yield ptrId; } }() } async removeItem(key: string): Promise { + this.#existingItemsCache.delete(key) await this.#query('DELETE FROM ?? WHERE ??=?;', [this.#metaTables.items.name, "key", key]) } async getItemValueDXB(key: string): Promise { - const encoded = (await this.#queryFirst<{value: string}>( + const encoded = (await this.#queryFirst<{value: string, ptrId: string}>( new Query() .table(this.#metaTables.items.name) - .select("value") + .select("value", `${this.#pointerMysqlColumnName} as ptrId`) .where(Where.eq("key", key)) .build() )); - if (!encoded || !encoded.value) return null; - else return this.#stringToBinary(encoded.value); + if (encoded?.ptrId) return Compiler.compile(`$${encoded.ptrId}`, undefined, undefined, false) as Promise; + else if (encoded?.value) return this.#stringToBinary(encoded.value); + else return null; } async setItemValueDXB(key: string, value: ArrayBuffer) { const stringBinary = this.#binaryToString(value) @@ -629,23 +743,27 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // this.log?.("update " + pointer.id + " - " + pointer.type, partialUpdateKey, await this.#pointerEntryExists(pointer)) // new full insert - if (partialUpdateKey === NOT_EXISTING || !await this.#pointerEntryExists(pointer)) { + if (partialUpdateKey === NOT_EXISTING || !await this.hasPointer(pointer.id)) { return this.#insertPointer(pointer) } // partial update else { if (typeof partialUpdateKey !== "string") throw new Error("invalid key type for SQL table: " + Datex.Type.ofValue(partialUpdateKey)) - await this.#updatePointer(pointer, [partialUpdateKey]) const dependencies = new Set() // add all pointer properties to dependencies + // dependencies must be added to database before the update to prevent foreign key constraint errors + const promises = [] for (const [name, {foreignPtr}] of this.#iterateTableColumns(pointer.type)) { if (foreignPtr) { const ptr = Pointer.pointerifyValue(pointer.getProperty(name)); if (ptr instanceof Pointer) { dependencies.add(ptr) + promises.push(Storage.setPointer(ptr)) } } } + await Promise.all(promises) + await this.#updatePointer(pointer, [partialUpdateKey]) return dependencies; } } @@ -662,7 +780,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const table = await this.#getPointerTable(pointerId); if (!table) { logger.error("No table found for pointer " + pointerId); - return null; + return NOT_EXISTING; } // is raw pointer @@ -674,7 +792,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) .build() ))?.value; - return value ? Runtime.decodeValue(this.#stringToBinary(value), outer_serialized) : null; + return value ? Runtime.decodeValue(this.#stringToBinary(value), outer_serialized) : NOT_EXISTING; } // is templated pointer @@ -682,10 +800,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const type = await this.#getTypeForTable(table); if (!type) { logger.error("No type found for table " + table); - return null; + return NOT_EXISTING; } const object = await this.#getTemplatedPointerObject(pointerId, table); - if (!object) return null; + if (!object) return NOT_EXISTING; // resolve foreign pointers const columns = await this.#getTableColumns(table); @@ -718,6 +836,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async removePointer(pointerId: string): Promise { + this.#existingPointersCache.delete(pointerId) // get table where pointer is stored const table = await this.#getPointerTable(pointerId); if (table) { @@ -754,9 +873,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // check if raw pointer, otherwise not yet supported const table = await this.#getPointerTable(pointerId); if (table == this.#metaTables.rawPointers.name) { - await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [table, [this.#pointerMysqlColumnName, "value"], [pointerId, value], value]) - // add to pointer mapping - await this.#updatePointerMapping(pointerId, this.#metaTables.rawPointers.name) + const {result} = await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [table, [this.#pointerMysqlColumnName, "value"], [pointerId, value], value], true) + console.log("affectedRows", result.affectedRows) + // is newly inserted, add to pointer mapping + if (result.affectedRows == 1) await this.#updatePointerMapping(pointerId, this.#metaTables.rawPointers.name) } else { logger.error("Setting raw dxb value for templated pointer is not yet supported in SQL storage (pointer: " + pointerId + ", table: " + table + ")"); @@ -764,6 +884,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async hasPointer(pointerId: string): Promise { + if (this.#existingPointersCache.has(pointerId)) return true; const count = (await this.#queryFirst<{COUNT: number}>( new Query() .table(this.#metaTables.pointerMapping.name) @@ -771,7 +892,13 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) .build() )); - return (!!count) && count.COUNT > 0; + const exists = !!count && count.COUNT > 0; + if (exists) { + this.#existingPointersCache.add(pointerId); + // delete from cache after 2 minutes + setTimeout(()=>this.#existingPointersCache.delete(pointerId), 1000*60*2) + } + return exists; } async clear() { diff --git a/storage/storage.ts b/storage/storage.ts index e59a87c4..2681686b 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -35,11 +35,40 @@ export const site_suffix = (()=>{ })(); +export const comparatorKeys = [ + "=", "!=", ">", ">=", "<", "<=" +] as const + +type _MatchInput = T extends object ? + { + [K in keyof T]?: MatchInputValue + } : + T|T[] +type MatchInputValue = + _MatchInput| // exact match + _MatchInput[]| // or match + Partial> // comparison matches + +export type MatchInput = MatchInputValue + + export interface StorageLocation { name: string isAsync: boolean + /** + * This storage location supports exec conditions for get operations + */ supportsExecConditions?: boolean + /** + * This storage location supports prefix selection for get operations + */ + supportsPrefixSelection?: boolean + /** + * This storage location supports match selection for get operations + * Must implement supportsMatchForType if true + */ + supportsMatchSelection?: boolean isSupported(): boolean onAfterExit?(): void // called when deno process exits @@ -51,7 +80,7 @@ export interface StorageLocation|void getItemValueDXB(key:string): Promise|ArrayBuffer|null setItemValueDXB(key:string, value: ArrayBuffer):Promise|void - getItemKeys(): Promise> | Generator + getItemKeys(prefix?:string): Promise> | Generator setPointer(pointer:Pointer, partialUpdateKey: unknown|typeof NOT_EXISTING): Promise>|Set getPointerValue(pointerId:string, outer_serialized:boolean, conditions?:ExecConditions):Promise|unknown @@ -60,6 +89,9 @@ export interface StorageLocation> | Generator getPointerValueDXB(pointerId:string): Promise|ArrayBuffer|null setPointerValueDXB(pointerId:string, value: ArrayBuffer):Promise|void + + supportsMatchForType?(type: Type): Promise|boolean + matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, limit: number): Promise|T[] clear(): Promise|void } @@ -74,7 +106,7 @@ export abstract class SyncStorageLocation implements StorageLocation abstract getItem(key:string, conditions?:ExecConditions): Promise|unknown abstract hasItem(key:string): boolean - abstract getItemKeys(): Generator + abstract getItemKeys(prefix?:string): Generator abstract removeItem(key: string): void abstract getItemValueDXB(key: string): ArrayBuffer|null @@ -89,6 +121,9 @@ export abstract class SyncStorageLocation implements StorageLocation(itemPrefix: string, valueType: Type, match: MatchInput, limit: number): T[] + abstract clear(): void } @@ -102,7 +137,7 @@ export abstract class AsyncStorageLocation implements StorageLocation> abstract getItem(key:string, conditions?:ExecConditions): Promise abstract hasItem(key:string): Promise - abstract getItemKeys(): Promise> + abstract getItemKeys(prefix?:string): Promise> abstract removeItem(key: string): Promise abstract getItemValueDXB(key: string): Promise @@ -117,6 +152,9 @@ export abstract class AsyncStorageLocation implements StorageLocation abstract hasPointer(pointerId: string): Promise + supportsMatchForType?(type: Type): Promise + matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, limit: number): Promise + abstract clear(): Promise } @@ -416,6 +454,7 @@ export class Storage { static setItem(key:string, value:any, listen_for_pointer_changes = true, location:StorageLocation|null|undefined = this.#primary_location):Promise|boolean { Storage.cache.set(key, value); // save in cache + console.warn("SETITEM",key,value) // cache deletion does not work, problems with storage item backup // setTimeout(()=>Storage.cache.delete(key), 10000); @@ -873,16 +912,16 @@ export class Storage { return NOT_EXISTING; } - public static async getItemKeys(location?:StorageLocation){ + public static async getItemKeys(location?:StorageLocation, prefix?: string){ // for specific location - if (location) return location.getItemKeys(); + if (location) return location.getItemKeys(prefix); // ... iterate over keys from all locations const generators = []; for (const location of this.#locations.keys()) { - generators.push(await location.getItemKeys()) + generators.push(await location.getItemKeys(prefix)) } return (function*(){ @@ -900,7 +939,7 @@ export class Storage { public static async getItemKeysStartingWith(prefix:string, location?:StorageLocation) { - const keyIterator = await Storage.getItemKeys(location); + const keyIterator = await Storage.getItemKeys(location, prefix); return (function*(){ for (const key of keyIterator) { if (key.startsWith(prefix)) yield key; @@ -909,7 +948,7 @@ export class Storage { } public static async getItemCountStartingWith(prefix:string, location?:StorageLocation) { - const keyIterator = await Storage.getItemKeys(location); + const keyIterator = await Storage.getItemKeys(location, prefix); let count = 0; for (const key of keyIterator) { if (key.startsWith(prefix)) count++; @@ -917,6 +956,15 @@ export class Storage { return count } + public static async supportsMatchQueries(type: Type) { + return (this.#primary_location?.supportsMatchSelection && await this.#primary_location?.supportsMatchForType!(type)) ?? false; + } + + public static itemMatchQuery(itemPrefix: string, valueType:Type, match: MatchInput, limit = Infinity) { + if (!this.#primary_location?.supportsMatchSelection) return []; + return this.#primary_location.matchQuery!(itemPrefix, valueType, match, limit); + } + public static async getPointerIds(location?:StorageLocation){ @@ -992,6 +1040,7 @@ export class Storage { const val = await location.getItem(key, conditions); if (val == NOT_EXISTING) return NOT_EXISTING; + console.warn("GETFRMLOC",key,val) Storage.cache.set(key, val); await this.initItemFromTrustedLocation(key, val, location) @@ -1000,7 +1049,7 @@ export class Storage { public static async hasItem(key:string, location?:StorageLocation):Promise { - + if (Storage.cache.has(key)) return true; // get from cache // try to find item at a storage location diff --git a/types/storage-map.ts b/types/storage-map.ts index 47c42051..39171430 100644 --- a/types/storage-map.ts +++ b/types/storage-map.ts @@ -39,6 +39,7 @@ export class StorageWeakMap { #_pointer?: Pointer; get #pointer() { if (!this.#_pointer) this.#_pointer = Pointer.getByValue(this); + if (!this.#_pointer) throw new Error(this.constructor.name + " not bound to a pointer") return this.#_pointer; } @@ -88,6 +89,7 @@ export class StorageWeakMap { if (!this.allowNonPointerObjectValues) { value = this.#pointer.proxifyChild("", value); } + console.log("SET>",storage_key, value) this.activateCacheTimeout(storage_key); return Storage.setItem(storage_key, value) } diff --git a/types/storage-set.ts b/types/storage-set.ts index a67cfee0..2cf7bac1 100644 --- a/types/storage-set.ts +++ b/types/storage-set.ts @@ -36,6 +36,7 @@ export class StorageWeakSet { #_pointer?: Pointer; get #pointer() { if (!this.#_pointer) this.#_pointer = Pointer.getByValue(this); + if (!this.#_pointer) throw new Error(this.constructor.name + " not bound to a pointer") return this.#_pointer; } @@ -49,7 +50,7 @@ export class StorageWeakSet { return set; } - protected get _prefix() { + get _prefix() { if (!this.#prefix) this.#prefix = 'dxset::'+(this as any)[DX_PTR].idString()+'.'; return this.#prefix; } @@ -114,11 +115,6 @@ export class StorageWeakSet { */ export class StorageSet extends StorageWeakSet { - #size_key?: string - protected get _size_key() { - if (!this.#size_key) this.#size_key = 'dxset.size::'+(this as any)[DX_PTR].idString()+'.'; - return this.#size_key; - } #size?: number; @@ -139,27 +135,20 @@ export class StorageSet extends StorageWeakSet { * Sets this.#size to the correct value determined from storage. */ async #determineSizeFromStorage() { - const cachedSize = await Storage.getItem(this._size_key); const calculatedSize = await Storage.getItemCountStartingWith(this._prefix); - if (cachedSize !== calculatedSize) { - if (cachedSize != undefined) - logger.warn(`Size mismatch for StorageSet (${(this as any)[DX_PTR].idString()}) detected. Setting size to ${calculatedSize}`) - await this.#updateSize(calculatedSize); - } - else this.#size = calculatedSize; + this.#updateSize(calculatedSize); } - async #updateSize(newSize: number) { + #updateSize(newSize: number) { this.#size = newSize; - await Storage.setItem(this._size_key, newSize); } async #incrementSize() { - await this.#updateSize(await this.getSize() + 1); + this.#updateSize(await this.getSize() + 1); } async #decrementSize() { - await this.#updateSize(await this.getSize() - 1); + this.#updateSize(await this.getSize() - 1); } diff --git a/utils/match.ts b/utils/match.ts new file mode 100644 index 00000000..56df9927 --- /dev/null +++ b/utils/match.ts @@ -0,0 +1,160 @@ +import { StorageSet } from "../types/storage_set.ts"; +import { Type } from "../types/type.ts"; +import { Class } from "./global_types.ts"; +import { MatchInput, Storage, comparatorKeys } from "../storage/storage.ts"; + +export type { MatchInput } from "../storage/storage.ts"; + +/** + * Returns all entries of a StorageSet that match the given match descriptor. + * @param inputSet + * @param match + * @param limit + * @returns + */ +export async function match(inputSet: StorageSet, valueType:Class|Type, match: MatchInput, limit = Infinity) { + const found = new Set(); + const matchOrEntries = (match instanceof Array ? match : [match]).map(m => Object.entries(m)) as [keyof T, T[keyof T]][][]; + + if (!(valueType instanceof Type)) valueType = Type.getClassDatexType(valueType); + + // match queries supported + if (await Storage.supportsMatchQueries(valueType)) { + return Storage.itemMatchQuery(inputSet._prefix, valueType, match, limit); + } + + // fallback: match by iterating over all entries + + for await (const input of inputSet) { + // ors + for (const matchOrs of matchOrEntries) { + let isMatch = true; + for (const [key, value] of matchOrs) { + if (!_match(input[key], value)) { + isMatch = false; + break; + } + } + if (isMatch) found.add(input); + if (found.size >= limit) break; + } + + } + return found; +} + +function _match(value: unknown, match: unknown) { + const matchOrs = (match instanceof Array ? match : [match]); + + for (const matchEntry of matchOrs) { + let isMatch = true; + // is comparator object + if (typeof matchEntry === "object" && Object.keys(matchEntry).some(key => comparatorKeys.includes(key as any))) { + if (!compare(value, matchEntry)) { + isMatch = false; + break; + } + } + // nested object + else if (value && typeof value == "object") { + // identical object + if (value === matchEntry) isMatch = true; + // nested match + else if (matchEntry && typeof matchEntry === "object") { + for (const [key, val] of Object.entries(matchEntry)) { + // an object entry does not match + if (!_match((value as any)[key], val)) { + isMatch = false; + break; + } + } + } + else isMatch = false; + } + // primitive, other value + else isMatch = value === matchEntry; + + // match? + if (isMatch) return true; + } + + // no match found + return false; +} + + +function compare(value: unknown, comparatorObj: Partial>) { + let isMatch = true; + for (const [comparator, val] of Object.entries(comparatorObj)) { + // special comparison keys + if (comparator === "=") { + if (value != val) { + isMatch = false; + break; + } + else continue; + } + else if (comparator === "!=") { + if (value == val) { + isMatch = false; + break; + } + else continue; + } + else if (comparator === ">") { + if ((value as any) <= (val as any)) { + isMatch = false; + break; + } + else continue; + } + else if (comparator === ">=") { + if ((value as any) < (val as any)) { + isMatch = false; + break; + } + else continue; + } + else if (comparator === "<") { + if ((value as any) >= (val as any)) { + isMatch = false; + break; + } + else continue; + } + else if (comparator === "<=") { + if ((value as any) > (val as any)) { + isMatch = false; + break; + } + else continue; + } + } + + return isMatch; +} + + + +// match(users, [{ +// name: "John", +// age: [1,2,3], +// address: { +// email: "x@t" +// } +// }, {name: "yxyx"}]) + +/* +SELECT _ptr_id + +FROM __datex_items +JOIN Person ON __datex_items._ptr_id = Person._ptr_id +JOIN Occupation ON Occupation._ptr_id = Person.occupation + +WHERE `key` LIKE "dxset::$D505B7E7C20Ex4E0749C88DE8EB%" +AND `first_name` LIKE "S%" +AND Occupation.degree = "Diplom (FH)" +AND Occupation.degree = "Diplom (FH)" +AND Occupation.degree = "Diplom (FH)" +AND Occupation.degree = "Diplom (FH)" +*/ \ No newline at end of file From c9a1a9296f522923160d39841f36fac1c17902e5 Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 21 Feb 2024 17:53:19 +0100 Subject: [PATCH 12/56] improve match queries --- storage/storage-locations/sql-db.ts | 87 +++++++--- storage/storage.ts | 239 +++++++++++++++++++--------- types/storage-map.ts | 50 +++++- types/storage-set.ts | 10 +- utils/match.ts | 11 +- 5 files changed, 294 insertions(+), 103 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 55c2b19e..98b60597 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -17,6 +17,9 @@ import { Type } from "../../types/type.ts"; import { TypedArray } from "../../utils/global_values.ts"; import { MessageLogger } from "../../utils/message_logger.ts"; import { Join } from "https://deno.land/x/sql_builder@v1.9.2/join.ts"; +import { LazyPointer } from "../../runtime/lazy-pointer.ts"; +import { MatchOptions } from "../storage.ts"; +import { MatchResult } from "../storage.ts"; const logger = new Logger("SQL Storage"); @@ -399,7 +402,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } // this.log("cols", insertData) - await this.#query('INSERT INTO ?? ?? VALUES ?;', [table, Object.keys(insertData), Object.values(insertData)]) + // replace if entry already exists + + await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE '+Object.keys(insertData).map((key) => `\`${key}\` = ?`).join(', '), [table, Object.keys(insertData), Object.values(insertData), ...Object.values(insertData)]) + // await this.#query('INSERT INTO ?? ?? VALUES ?;', [table, Object.keys(insertData), Object.values(insertData)]) // add to pointer mapping await this.#updatePointerMapping(pointer.id, table) @@ -522,7 +528,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async #getTemplatedPointerValueDXB(pointerId: string, table?: string) { const string = await this.#getTemplatedPointerValueString(pointerId, table); if (!string) return null; - const compiled = await Compiler.compile(string, [], {sign: false, encrypt: false, to: Datex.Runtime.endpoint}, false) as ArrayBuffer; + const compiled = await Compiler.compile(string, [], {sign: false, encrypt: false, to: Datex.Runtime.endpoint, preemptive_pointer_init: false}, false) as ArrayBuffer; return compiled } @@ -557,34 +563,65 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return client_type === "deno"; } - async supportsMatchForType(type: Datex.Type): Promise { - return (await this.#getTableForType(type)) != null; + supportsMatchForType(type: Datex.Type) { + // only templated types are supported because they are stored in custom tables + return !!type.template } - async matchQuery(itemPrefix: string, valueType: Datex.Type, match: Datex.MatchInput, limit: number): Promise { + async matchQuery(itemPrefix: string, valueType: Datex.Type, match: Datex.MatchInput, options: Options): Promise> { const builder = new Query() .table(this.#metaTables.items.name) // .select(this.#pointerMysqlColumnName) - .select(`${this.#typeToTableName(valueType)}.${this.#pointerMysqlColumnName} as ptrId`) + .select(`SQL_CALC_FOUND_ROWS ${this.#typeToTableName(valueType)}.${this.#pointerMysqlColumnName} as ptrId`) .where(Where.like("key", itemPrefix + "%")) .join( Join.left(this.#typeToTableName(valueType)).on(`${this.#metaTables.items.name}.${this.#pointerMysqlColumnName}`, `${this.#typeToTableName(valueType)}.${this.#pointerMysqlColumnName}`) ) - if (isFinite(limit)) builder.limit(0, limit) + // limit, do limit later if options.returnPointerIds + if (options && (options.limit !== undefined && isFinite(options.limit) && !options.returnPointerIds)) { + builder.limit(options.offset ?? 0, options.limit) + } const joins = new Map() const where = this.buildQueryConditions(builder, match, joins, valueType) - builder.where(where); + if (where) builder.where(where); joins.forEach(join => builder.join(join)); - - const ptrIds = await this.#query<{ptrId:string}>(builder.build()) - return Promise.all(ptrIds.map(({ptrId}) => Storage.getPointer(ptrId))) + + const ptrIds = (await this.#query<{ptrId:string}>(builder.build())).map(({ptrId}) => ptrId) + const limitedPtrIds = options.returnPointerIds ? + // offset and limit manually after query + ptrIds.slice(options.offset ?? 0, options.limit ? (options.offset ?? 0) + options.limit : undefined) : + // use ptrIds returned from query (already limited) + ptrIds + + // TODO: atomic operations for multiple queries + const {foundRows} = await this.#queryFirst<{foundRows: number}>("SELECT FOUND_ROWS() as foundRows") ?? {foundRows: -1} + console.log("foundRows", foundRows) + + const result = new Set((await Promise.all(limitedPtrIds.map(ptrId => Pointer.load(ptrId)))).filter(ptr => { + if (ptr instanceof LazyPointer) { + logger.warn("Cannot return lazy pointer from match query (" + ptr.id + ")"); + return false; + } + return true; + }).map(ptr => (ptr as Pointer).val as T)) + + if (options?.returnAdvanced) { + return { + matches: result, + total: foundRows, + ...options?.returnPointerIds ? {pointerIds: new Set(ptrIds)} : {} + } as MatchResult; + } + else { + return result as MatchResult; + } } - private buildQueryConditions(builder: Query, match: unknown, joins: Map, valueType:Type, namespacedKey?: string, previousKey?: string): Where { + private buildQueryConditions(builder: Query, match: unknown, joins: Map, valueType:Type, namespacedKey?: string, previousKey?: string): Where|undefined { const matchOrs = match instanceof Array ? match : [match] const entryIdentifier = previousKey ? previousKey + '.' + namespacedKey : namespacedKey // address.street @@ -592,13 +629,12 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { let isPrimitiveArray = true; for (const or of matchOrs) { - if (typeof or == "object") { + if (typeof or == "object" || or === null) { isPrimitiveArray = false; break; } } - // only primitive array, use IN selector if (isPrimitiveArray) { if (!namespacedKey) throw new Error("missing namespacedKey"); @@ -610,11 +646,18 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const wheresOr = [] for (const or of matchOrs) { - if (typeof or == "object") { + // regex + if (or instanceof RegExp) { + if (!namespacedKey) throw new Error("missing namespacedKey"); + wheresOr.push(Where.expr(`${entryIdentifier!} REGEXP ?`, or.source)) + } + + else if (typeof or == "object" && or !== null) { // is pointer const ptr = Pointer.pointerifyValue(or); if (ptr instanceof Pointer) { + if (!namespacedKey) throw new Error("missing namespacedKey"); wheresOr.push(Where.eq(entryIdentifier!, ptr.id)) continue; } @@ -629,19 +672,21 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { valueType = valueType.template[namespacedKey]; } - const whereAnds = [] + const whereAnds:Where[] = [] for (const [key, value] of Object.entries(or)) { // const nestedKey = namespacedKey ? namespacedKey + "." + key : key; - whereAnds.push(this.buildQueryConditions(builder, value, joins, valueType, key, namespacedKey)) + const condition = this.buildQueryConditions(builder, value, joins, valueType, key, namespacedKey); + if (condition) whereAnds.push(condition) } - wheresOr.push(Where.and(...whereAnds)) + if (whereAnds.length > 1) wheresOr.push(Where.and(...whereAnds)) + else if (whereAnds.length) wheresOr.push(whereAnds[0]) } else { if (!namespacedKey) throw new Error("missing namespacedKey"); wheresOr.push(Where.eq(entryIdentifier!, or)) } } - return Where.or(...wheresOr) + if (wheresOr.length) return Where.or(...wheresOr) } } @@ -727,7 +772,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .where(Where.eq("key", key)) .build() )); - if (encoded?.ptrId) return Compiler.compile(`$${encoded.ptrId}`, undefined, undefined, false) as Promise; + if (encoded?.ptrId) return Compiler.compile(`$${encoded.ptrId}`, undefined, {sign: false, encrypt: false, to: Datex.Runtime.endpoint, preemptive_pointer_init: false}, false) as Promise; else if (encoded?.value) return this.#stringToBinary(encoded.value); else return null; } @@ -818,7 +863,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // is an object type with a template if (foreignPtr) { if (typeof object[colName] == "string") { - object[colName] = await Storage.getPointer(object[colName] as string, true); + object[colName] = await Pointer.load(object[colName] as string); } else { logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) diff --git a/storage/storage.ts b/storage/storage.ts index 2681686b..1ea44f33 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -39,11 +39,18 @@ export const comparatorKeys = [ "=", "!=", ">", ">=", "<", "<=" ] as const +type AtomicMatchInput = T | + ( + T extends string ? + RegExp : + never + ) + type _MatchInput = T extends object ? { [K in keyof T]?: MatchInputValue } : - T|T[] + AtomicMatchInput|AtomicMatchInput[] type MatchInputValue = _MatchInput| // exact match _MatchInput[]| // or match @@ -51,6 +58,48 @@ type MatchInputValue = export type MatchInput = MatchInputValue +export type MatchOptions = { + /** + * Maximum number of matches to return + */ + limit?: number, + /** + * Sort by key (e.g. address.street) + */ + sortBy?: string, + /** + * Sort in descending order (only if sortBy is set) + */ + sortDesc?: boolean, + /** + * Offset for match results + */ + offset?: number, + /** + * Return advanced match results (e.g. total count of matches) + */ + returnAdvanced?: boolean, + /** + * Return pointer ids of matched items + */ + returnPointerIds?: boolean, +} + +export type MatchResult = Options["returnAdvanced"] extends true ? + AdvancedMatchResult & ( + Options["returnPointerIds"] extends true ? + { + pointerIds: Set + } : + unknown + ) : + Set + +export type AdvancedMatchResult = { + total: number, + pointerIds?: Set, + matches: Set +} export interface StorageLocation { @@ -91,7 +140,7 @@ export interface StorageLocation|void supportsMatchForType?(type: Type): Promise|boolean - matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, limit: number): Promise|T[] + matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): Promise>|MatchResult clear(): Promise|void } @@ -121,8 +170,8 @@ export abstract class SyncStorageLocation implements StorageLocation(itemPrefix: string, valueType: Type, match: MatchInput, limit: number): T[] + supportsMatchForType?(type: Type): boolean + matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): MatchResult abstract clear(): void } @@ -152,8 +201,8 @@ export abstract class AsyncStorageLocation implements StorageLocation abstract hasPointer(pointerId: string): Promise - supportsMatchForType?(type: Type): Promise - matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, limit: number): Promise + supportsMatchForType?(type: Type): Promise|boolean + matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): Promise> abstract clear(): Promise } @@ -179,7 +228,11 @@ type StorageSnapshotOptions = { /** * Only display items (and related pointers) that contain the given string in their key */ - itemFilter?: string + itemFilter?: string, + /** + * Only display general information about storage data, no items or pointers + */ + onlyHeaders?: boolean } export class Storage { @@ -454,7 +507,6 @@ export class Storage { static setItem(key:string, value:any, listen_for_pointer_changes = true, location:StorageLocation|null|undefined = this.#primary_location):Promise|boolean { Storage.cache.set(key, value); // save in cache - console.warn("SETITEM",key,value) // cache deletion does not work, problems with storage item backup // setTimeout(()=>Storage.cache.delete(key), 10000); @@ -468,21 +520,23 @@ export class Storage { static async setItemAsync(location:AsyncStorageLocation, key: string, value: unknown,listen_for_pointer_changes: boolean) { this.setDirty(location, true) + const itemExisted = await location.hasItem(key); // store value (might be pointer reference) const dependencies = await location.setItem(key, value); if (Pointer.is_local) this.checkUnresolvedLocalDependenciesForItem(key, value, dependencies); this.updateItemDependencies(key, [...dependencies].map(p=>p.id)); await this.saveDependencyPointersAsync(dependencies, listen_for_pointer_changes, location); this.setDirty(location, false) - return true; + return itemExisted; } static setItemSync(location:SyncStorageLocation, key: string, value: unknown,listen_for_pointer_changes: boolean) { + const itemExisted = location.hasItem(key); const dependencies = location.setItem(key, value); if (Pointer.is_local) this.checkUnresolvedLocalDependenciesForItem(key, value, dependencies); this.updateItemDependencies(key, [...dependencies].map(p=>p.id)); this.saveDependencyPointersSync(dependencies, listen_for_pointer_changes, location); - return true; + return itemExisted; } /** @@ -960,9 +1014,10 @@ export class Storage { return (this.#primary_location?.supportsMatchSelection && await this.#primary_location?.supportsMatchForType!(type)) ?? false; } - public static itemMatchQuery(itemPrefix: string, valueType:Type, match: MatchInput, limit = Infinity) { - if (!this.#primary_location?.supportsMatchSelection) return []; - return this.#primary_location.matchQuery!(itemPrefix, valueType, match, limit); + public static itemMatchQuery(itemPrefix: string, valueType:Type, match: MatchInput, options?:Options) { + options ??= {} as Options; + if (!this.#primary_location?.supportsMatchSelection) throw new Error("Primary storage location does not support match queries"); + return this.#primary_location!.matchQuery!(itemPrefix, valueType, match, options); } @@ -1040,7 +1095,6 @@ export class Storage { const val = await location.getItem(key, conditions); if (val == NOT_EXISTING) return NOT_EXISTING; - console.warn("GETFRMLOC",key,val) Storage.cache.set(key, val); await this.initItemFromTrustedLocation(key, val, location) @@ -1069,6 +1123,9 @@ export class Storage { else return false; } + /** + * Remove an item from storage, returns true if the item existed + */ public static async removeItem(key:string, location?:StorageLocation):Promise { logger.debug("Removing item '" + key + "' from storage" + (location ? " (" + location.name + ")" : "")) @@ -1234,7 +1291,7 @@ export class Storage { } - public static async printSnapshot(options: StorageSnapshotOptions = {internalItems: false, expandStorageMapsAndSets: true}) { + public static async printSnapshot(options: StorageSnapshotOptions = {internalItems: false, expandStorageMapsAndSets: true, onlyHeaders: false}) { const {items, pointers} = await this.getSnapshot(options); const COLOR_PTR = `\x1b[38;2;${[65,102,238].join(';')}m` @@ -1256,13 +1313,19 @@ export class Storage { string = ESCAPE_SEQUENCES.BOLD+"Pointers\n\n"+ESCAPE_SEQUENCES.RESET string += `${ESCAPE_SEQUENCES.ITALIC}A list of all pointers stored in any storage location. Pointers are only stored as long as they are referenced somewhere else in the storage.\n\n${ESCAPE_SEQUENCES.RESET}` - for (const [key, storageMap] of pointers.snapshot) { - // check if stored in all locations, otherwise print in which location it is stored (functional programming) - const locations = [...storageMap.keys()] - const storedInAll = [...this.#locations.keys()].every(l => locations.includes(l)); - - const value = [...storageMap.values()][0]; - string += ` • ${COLOR_PTR}$${key}${ESCAPE_SEQUENCES.GREY}${storedInAll ? "" : (` (only in ${locations.map(l=>l.name).join(",")})`)} = ${value.replaceAll("\n", "\n ")}\n` + const pointersInMemory = [...pointers.snapshot.keys()].filter(id => Pointer.get(id)).length; + string += `\nTotal: ${ESCAPE_SEQUENCES.BOLD}${pointers.snapshot.size}${ESCAPE_SEQUENCES.RESET} pointers` + string += `\nIn memory: ${ESCAPE_SEQUENCES.BOLD}${pointersInMemory}${ESCAPE_SEQUENCES.RESET} pointers\n\n` + + if (!options.onlyHeaders) { + for (const [key, storageMap] of pointers.snapshot) { + // check if stored in all locations, otherwise print in which location it is stored (functional programming) + const locations = [...storageMap.keys()] + const storedInAll = [...this.#locations.keys()].every(l => locations.includes(l)); + + const value = [...storageMap.values()][0]; + string += ` • ${COLOR_PTR}$${key}${ESCAPE_SEQUENCES.GREY}${storedInAll ? "" : (` (only in ${locations.map(l=>l.name).join(",")})`)} = ${value.replaceAll("\n", "\n ")}\n` + } } console.log(string+"\n"); @@ -1270,13 +1333,15 @@ export class Storage { string = ESCAPE_SEQUENCES.BOLD+"Items\n\n"+ESCAPE_SEQUENCES.RESET string += `${ESCAPE_SEQUENCES.ITALIC}A list of all named items stored in any storage location.\n\n${ESCAPE_SEQUENCES.RESET}` - for (const [key, storageMap] of items.snapshot) { - // check if stored in all locations, otherwise print in which location it is stored (functional programming) - const locations = [...storageMap.keys()] - const storedInAll = [...this.#locations.keys()].every(l => locations.includes(l)); - - const value = [...storageMap.values()][0]; - string += ` • ${key}${ESCAPE_SEQUENCES.GREY}${storedInAll ? "" : (` (only in ${locations.map(l=>l.name).join(",")})`)} = ${value}\n` + if (!options.onlyHeaders) { + for (const [key, storageMap] of items.snapshot) { + // check if stored in all locations, otherwise print in which location it is stored (functional programming) + const locations = [...storageMap.keys()] + const storedInAll = [...this.#locations.keys()].every(l => locations.includes(l)); + + const value = [...storageMap.values()][0]; + string += ` • ${key}${ESCAPE_SEQUENCES.GREY}${storedInAll ? "" : (` (only in ${locations.map(l=>l.name).join(",")})`)} = ${value}\n` + } } console.log(string+"\n"); @@ -1287,37 +1352,39 @@ export class Storage { let rc_string = "" let item_deps_string = "" let pointer_deps_string = "" - for (let i = 0, len = localStorage.length; i < len; ++i ) { - const key = localStorage.key(i)!; - if (key.startsWith(this.rc_prefix)) { - const ptrId = key.substring(this.rc_prefix.length); - const count = this.getReferenceCount(ptrId); - rc_string += `\x1b[0m • ${key} = ${COLOR_NUMBER}${count}\n` - } - else if (key.startsWith(this.item_deps_prefix)) { - const depsRaw = localStorage.getItem(key); - // single entry - if (!depsRaw?.includes(",")) { - item_deps_string += `\x1b[0m • ${key} = (${COLOR_PTR}${depsRaw}\x1b[0m)\n` + if (!options.onlyHeaders) { + for (let i = 0, len = localStorage.length; i < len; ++i ) { + const key = localStorage.key(i)!; + if (key.startsWith(this.rc_prefix)) { + const ptrId = key.substring(this.rc_prefix.length); + const count = this.getReferenceCount(ptrId); + rc_string += `\x1b[0m • ${key} = ${COLOR_NUMBER}${count}\n` } - // multiple entries - else { - let deps = localStorage.getItem(key)!.split(",").join(`\x1b[0m,\n ${COLOR_PTR}$`) - if (deps) deps = ` ${COLOR_PTR}$`+deps - item_deps_string += `\x1b[0m • ${key} = (\n${COLOR_PTR}${deps}\x1b[0m\n )\n` - } - } - else if (key.startsWith(this.pointer_deps_prefix)) { - const depsRaw = localStorage.getItem(key); - // single entry - if (!depsRaw?.includes(",")) { - pointer_deps_string += `\x1b[0m • ${key} = (${COLOR_PTR}${depsRaw}\x1b[0m)\n` + else if (key.startsWith(this.item_deps_prefix)) { + const depsRaw = localStorage.getItem(key); + // single entry + if (!depsRaw?.includes(",")) { + item_deps_string += `\x1b[0m • ${key} = (${COLOR_PTR}${depsRaw}\x1b[0m)\n` + } + // multiple entries + else { + let deps = localStorage.getItem(key)!.split(",").join(`\x1b[0m,\n ${COLOR_PTR}$`) + if (deps) deps = ` ${COLOR_PTR}$`+deps + item_deps_string += `\x1b[0m • ${key} = (\n${COLOR_PTR}${deps}\x1b[0m\n )\n` + } } - // multiple entries - else { - let deps = localStorage.getItem(key)!.split(",").join(`\x1b[0m,\n ${COLOR_PTR}$`) - if (deps) deps = ` ${COLOR_PTR}$`+deps - pointer_deps_string += `\x1b[0m • ${key} = (\n${COLOR_PTR}${deps}\x1b[0m\n )\n` + else if (key.startsWith(this.pointer_deps_prefix)) { + const depsRaw = localStorage.getItem(key); + // single entry + if (!depsRaw?.includes(",")) { + pointer_deps_string += `\x1b[0m • ${key} = (${COLOR_PTR}${depsRaw}\x1b[0m)\n` + } + // multiple entries + else { + let deps = localStorage.getItem(key)!.split(",").join(`\x1b[0m,\n ${COLOR_PTR}$`) + if (deps) deps = ` ${COLOR_PTR}$`+deps + pointer_deps_string += `\x1b[0m • ${key} = (\n${COLOR_PTR}${deps}\x1b[0m\n )\n` + } } } } @@ -1330,17 +1397,21 @@ export class Storage { if (pointers.inconsistencies.size > 0 || items.inconsistencies.size > 0) { string = ESCAPE_SEQUENCES.BOLD+"Inconsistencies\n\n"+ESCAPE_SEQUENCES.RESET string += `${ESCAPE_SEQUENCES.ITALIC}Inconsistencies between storage locations don't necessarily indicate that something is wrong. They can occur when a storage location is not updated immediately (e.g. when only using SAVE_ON_EXIT).\n\n${ESCAPE_SEQUENCES.RESET}` - for (const [key, storageMap] of pointers.inconsistencies) { - for (const [location, value] of storageMap) { - string += ` • ${COLOR_PTR}$${key}${ESCAPE_SEQUENCES.GREY} (${(location.name+")").padEnd(15, " ")} = ${value.replaceAll("\n", "\n ")}\n` + + + if (!options.onlyHeaders) { + for (const [key, storageMap] of pointers.inconsistencies) { + for (const [location, value] of storageMap) { + string += ` • ${COLOR_PTR}$${key}${ESCAPE_SEQUENCES.GREY} (${(location.name+")").padEnd(15, " ")} = ${value.replaceAll("\n", "\n ")}\n` + } + string += `\n` } - string += `\n` - } - for (const [key, storageMap] of items.inconsistencies) { - for (const [location, value] of storageMap) { - string += ` • ${key}${ESCAPE_SEQUENCES.GREY} (${(location.name+")").padEnd(15, " ")} = ${value}` + for (const [key, storageMap] of items.inconsistencies) { + for (const [location, value] of storageMap) { + string += ` • ${key}${ESCAPE_SEQUENCES.GREY} (${(location.name+")").padEnd(15, " ")} = ${value}` + } + string += `\n` } - string += `\n` } console.info(string+"\n"); @@ -1389,6 +1460,7 @@ export class Storage { if (ptr.val instanceof StorageMap) { const map = ptr.val; const keyIterator = await this.getItemKeysStartingWith((map as any)._prefix) + const pointerIds = new Set(); let inner = ""; for await (const key of keyIterator) { const valString = await this.getItemDecompiled(key, true, location); @@ -1405,19 +1477,36 @@ export class Storage { // additional pointer ids included in value or key if (allowedPointerIds) { - const matches = [...valString.match(/\$[a-zA-Z0-9]+/g)??[], ...keyString.match(/\$[a-zA-Z0-9]+/g)??[]]; - for (const match of matches) { + const valMatches = valString.match(/\$[a-zA-Z0-9]+/g)??[] + const keyMatches = keyString.match(/\$[a-zA-Z0-9]+/g)??[]; + + for (const match of valMatches) { + const id = match.substring(1); + pointerIds.add(id) + if (!allowedPointerIds.has(id)) additionalEntries.add(id); + } + for (const match of keyMatches) { const id = match.substring(1); if (!allowedPointerIds.has(id)) additionalEntries.add(id); + } } } + + // size in memory / total size + const totalSize = await (ptr.val as StorageMap).getSize(); + const totalDirectPointerSize = pointerIds.size; + const inMemoryPointersSize= [...pointerIds].filter(id => Pointer.get(id)).length; + const sizeInfo = ` ${ESCAPE_SEQUENCES.GREY}total size: ${totalSize}, in memory: ${inMemoryPointersSize}/${totalDirectPointerSize} pointers${ESCAPE_SEQUENCES.RESET}\n` + // substring: remove last \n - if (inner) storageMap.set(location, "\x1b[38;2;50;153;220m \x1b[0m{\n"+inner.substring(0, inner.length-1)+"\x1b[0m\n}") + if (inner) storageMap.set(location, "\x1b[38;2;50;153;220m \x1b[0m{\n"+sizeInfo+inner.substring(0, inner.length-1)+"\x1b[0m\n}") } else if (ptr.val instanceof StorageSet) { const set = ptr.val; const keyIterator = await this.getItemKeysStartingWith((set as any)._prefix) + const pointerIds = new Set(); + let inner = ""; for await (const key of keyIterator) { const valString = await this.getItemDecompiled(key, true, location); @@ -1432,12 +1521,20 @@ export class Storage { const matches = valString.match(/\$[a-zA-Z0-9]+/g)??[]; for (const match of matches) { const id = match.substring(1); + pointerIds.add(id) if (!allowedPointerIds.has(id)) additionalEntries.add(id); } } } + + // size in memory / total size + const totalSize = await (ptr.val as StorageSet).getSize(); + const totalDirectPointerSize = pointerIds.size; + const inMemoryPointersSize= [...pointerIds].filter(id => Pointer.get(id)).length; + const sizeInfo = ` ${ESCAPE_SEQUENCES.GREY}total size: ${totalSize}, in memory: ${inMemoryPointersSize}/${totalDirectPointerSize} pointers${ESCAPE_SEQUENCES.RESET}\n` + // substring: remove last \n - if (inner) storageMap.set(location, "\x1b[38;2;50;153;220m \x1b[0m{\n"+inner.substring(0, inner.length-1)+"\x1b[0m\n}") + if (inner) storageMap.set(location, "\x1b[38;2;50;153;220m \x1b[0m{\n"+sizeInfo+inner.substring(0, inner.length-1)+"\x1b[0m\n}") } } } diff --git a/types/storage-map.ts b/types/storage-map.ts index 39171430..08606f75 100644 --- a/types/storage-map.ts +++ b/types/storage-map.ts @@ -84,14 +84,15 @@ export class StorageWeakMap { const storage_key = await this.getStorageKey(key); return this._set(storage_key, value); } - protected _set(storage_key:string, value:V) { + protected async _set(storage_key:string, value:V) { // proxify value if (!this.allowNonPointerObjectValues) { value = this.#pointer.proxifyChild("", value); } console.log("SET>",storage_key, value) this.activateCacheTimeout(storage_key); - return Storage.setItem(storage_key, value) + await Storage.setItem(storage_key, value) + return this; } protected activateCacheTimeout(storage_key:string){ @@ -130,14 +131,51 @@ export class StorageMap extends StorageWeakMap { #key_prefix = 'key.' - override async set(key: K, value: V): Promise { + #size?: number; + + get size() { + if (this.#size == undefined) throw new Error("size not yet available. use getSize() instead"); + return this.#size; + } + + async getSize() { + if (this.#size != undefined) return this.#size; + else { + await this.#determineSizeFromStorage(); + return this.#size! + } + } + + /** + * Sets this.#size to the correct value determined from storage. + */ + async #determineSizeFromStorage() { + const calculatedSize = await Storage.getItemCountStartingWith(this._prefix); + this.#updateSize(calculatedSize); + } + + #updateSize(newSize: number) { + this.#size = newSize; + } + + async #incrementSize() { + this.#updateSize(await this.getSize() + 1); + } + + async #decrementSize() { + this.#updateSize(await this.getSize() - 1); + } + + override async set(key: K, value: V): Promise { const storage_key = await this.getStorageKey(key); const storage_item_key = this.#key_prefix + storage_key; // store value await this._set(storage_key, value); // store key this.activateCacheTimeout(storage_item_key); - return Storage.setItem(storage_item_key, key) + const alreadyExisted = await Storage.setItem(storage_item_key, key); + if (!alreadyExisted) await this.#incrementSize(); + return this; } override async delete(key: K) { @@ -146,7 +184,9 @@ export class StorageMap extends StorageWeakMap { // delete value await this._delete(storage_key); // delete key - return Storage.removeItem(storage_item_key) + const existed = await Storage.removeItem(storage_item_key) + if (existed) await this.#decrementSize(); + return existed; } /** diff --git a/types/storage-set.ts b/types/storage-set.ts index 2cf7bac1..724da55b 100644 --- a/types/storage-set.ts +++ b/types/storage-set.ts @@ -3,8 +3,12 @@ import { Compiler } from "../compiler/compiler.ts"; import { DX_PTR } from "../runtime/constants.ts"; import { Pointer } from "../runtime/pointers.ts"; -import { Storage } from "../storage/storage.ts"; +import { MatchResult, Storage } from "../storage/storage.ts"; import { Logger } from "../utils/logger.ts"; +import { MatchInput, match } from "../utils/match.ts"; +import { Class } from "../utils/global_types.ts"; +import { MatchOptions } from "../utils/match.ts"; +import { Type } from "./type.ts"; const logger = new Logger("StorageSet"); @@ -242,4 +246,8 @@ export class StorageSet extends StorageWeakSet { } } + match(valueType:Class|Type, matchInput: MatchInput, options?: Options): Promise> { + return match(this as unknown as StorageSet, valueType, matchInput, options) + } + } \ No newline at end of file diff --git a/utils/match.ts b/utils/match.ts index 56df9927..3ff77357 100644 --- a/utils/match.ts +++ b/utils/match.ts @@ -1,9 +1,9 @@ import { StorageSet } from "../types/storage_set.ts"; import { Type } from "../types/type.ts"; import { Class } from "./global_types.ts"; -import { MatchInput, Storage, comparatorKeys } from "../storage/storage.ts"; +import { MatchInput, MatchResult, MatchOptions, Storage, comparatorKeys } from "../storage/storage.ts"; -export type { MatchInput } from "../storage/storage.ts"; +export type { MatchInput, MatchOptions, MatchResult } from "../storage/storage.ts"; /** * Returns all entries of a StorageSet that match the given match descriptor. @@ -12,7 +12,8 @@ export type { MatchInput } from "../storage/storage.ts"; * @param limit * @returns */ -export async function match(inputSet: StorageSet, valueType:Class|Type, match: MatchInput, limit = Infinity) { +export async function match(inputSet: StorageSet, valueType:Class|Type, match: MatchInput, options?: Options): Promise> { + options ??= {} as Options; const found = new Set(); const matchOrEntries = (match instanceof Array ? match : [match]).map(m => Object.entries(m)) as [keyof T, T[keyof T]][][]; @@ -20,7 +21,7 @@ export async function match(inputSet: StorageSet, valueType // match queries supported if (await Storage.supportsMatchQueries(valueType)) { - return Storage.itemMatchQuery(inputSet._prefix, valueType, match, limit); + return Storage.itemMatchQuery(inputSet._prefix, valueType, match, options); } // fallback: match by iterating over all entries @@ -36,7 +37,7 @@ export async function match(inputSet: StorageSet, valueType } } if (isMatch) found.add(input); - if (found.size >= limit) break; + if (found.size >= (options.limit??Infinity)) break; } } From 31895ff7be7118e21c3de46d4b32bc4a01245e75 Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 21 Feb 2024 18:30:48 +0100 Subject: [PATCH 13/56] fix date time conversion --- storage/storage-locations/sql-db.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 98b60597..683e1089 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -20,6 +20,7 @@ import { Join } from "https://deno.land/x/sql_builder@v1.9.2/join.ts"; import { LazyPointer } from "../../runtime/lazy-pointer.ts"; import { MatchOptions } from "../storage.ts"; import { MatchResult } from "../storage.ts"; +import { Time } from "../../types/time.ts"; const logger = new Logger("SQL Storage"); @@ -241,6 +242,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { * Does not validate if the table exists */ #typeToTableName(type: Datex.Type) { + if (!type.template) throw new Error("Cannot create table for non-templated type " + type) return type.namespace=="ext" ? type.name : `${type.namespace}_${type.name}`; } @@ -585,11 +587,17 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } const joins = new Map() - const where = this.buildQueryConditions(builder, match, joins, valueType) + const collectedTableTypes = new Set([valueType]) + const where = this.buildQueryConditions(builder, match, joins, collectedTableTypes, valueType) if (where) builder.where(where); joins.forEach(join => builder.join(join)); + // make sure all tables are created + for (const type of collectedTableTypes) { + await this.#getTableForType(type) + } + const ptrIds = (await this.#query<{ptrId:string}>(builder.build())).map(({ptrId}) => ptrId) const limitedPtrIds = options.returnPointerIds ? // offset and limit manually after query @@ -621,7 +629,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - private buildQueryConditions(builder: Query, match: unknown, joins: Map, valueType:Type, namespacedKey?: string, previousKey?: string): Where|undefined { + private buildQueryConditions(builder: Query, match: unknown, joins: Map, collectedTableTypes:Set, valueType:Type, namespacedKey?: string, previousKey?: string): Where|undefined { const matchOrs = match instanceof Array ? match : [match] const entryIdentifier = previousKey ? previousKey + '.' + namespacedKey : namespacedKey // address.street @@ -629,7 +637,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { let isPrimitiveArray = true; for (const or of matchOrs) { - if (typeof or == "object" || or === null) { + if (typeof or == "object" || or === null || or instanceof Date) { isPrimitiveArray = false; break; } @@ -652,7 +660,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { wheresOr.push(Where.expr(`${entryIdentifier!} REGEXP ?`, or.source)) } - else if (typeof or == "object" && or !== null) { + else if (typeof or == "object" && !(or == null || or instanceof Date)) { // is pointer const ptr = Pointer.pointerifyValue(or); @@ -664,6 +672,9 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // only enter after first recursion if (namespacedKey) { + collectedTableTypes.add(valueType); + collectedTableTypes.add(valueType.template[namespacedKey]); + const tableAName = this.#typeToTableName(valueType) + '.' + namespacedKey // User.address const tableBName = this.#typeToTableName(valueType.template[namespacedKey]); // Address const tableBIdentifier = namespacedKey + '.' + this.#pointerMysqlColumnName @@ -675,7 +686,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const whereAnds:Where[] = [] for (const [key, value] of Object.entries(or)) { // const nestedKey = namespacedKey ? namespacedKey + "." + key : key; - const condition = this.buildQueryConditions(builder, value, joins, valueType, key, namespacedKey); + const condition = this.buildQueryConditions(builder, value, joins, collectedTableTypes, valueType, key, namespacedKey); if (condition) whereAnds.push(condition) } if (whereAnds.length > 1) wheresOr.push(Where.and(...whereAnds)) @@ -855,10 +866,15 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (!columns) throw new Error("No columns found for table " + table) for (const [colName, {foreignPtr, type}] of columns.entries()) { + // custom conversions: // convert blob strings to ArrayBuffer if (type == "blob" && typeof object[colName] == "string") { object[colName] = this.#stringToBinary(object[colName] as string) } + // convert Date ot Time + else if (object[colName] instanceof Date) { + object[colName] = new Time(object[colName] as Date) + } // is an object type with a template if (foreignPtr) { From 21386bbd8f77eb8180020fbf54428b179bf21b23 Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 22 Feb 2024 19:07:06 +0100 Subject: [PATCH 14/56] improve unit typing for quantities --- compiler/unit_codes.ts | 19 ++++++++++++++++--- types/quantity.ts | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/compiler/unit_codes.ts b/compiler/unit_codes.ts index 5060f55d..e8ee576c 100644 --- a/compiler/unit_codes.ts +++ b/compiler/unit_codes.ts @@ -174,6 +174,17 @@ export const UnitAliases = { } as const; +export type UnitAliasUnits = { + [k in keyof typeof UnitAliases]: typeof UnitAliases[k][2] +} + +// reverse mapping for all aliases of a unit e.g. Unit.SECOND -> "min" | "h" | "d" | "a" | "yr" +export type UnitAliasesMap = { + [key in UnitAliasUnits[keyof UnitAliasUnits]]: { + [k in keyof UnitAliasUnits]: UnitAliasUnits[k] extends key ? k : never + }[keyof UnitAliasUnits] +} + // prefixes ----------------------------------------------------------------------------------------- @@ -282,8 +293,10 @@ export type unit_symbol = unit_base_symbol | keyof typeof UnitCodeBySymbolShortF export type unit_prefix = keyof typeof UnitPrefixCodeBySymbol; -export type code_to_symbol = Combine; -export type symbol_to_code = Combine; +export type code_to_symbol = typeof UnitSymbol[C] | typeof UnitSymbolShortFormsByCode[C & keyof typeof UnitSymbolShortFormsByCode]; +export type symbol_to_code = typeof UnitCodeBySymbol & UnitAliasUnits & typeof UnitCodeBySymbolShortForms; + +export type symbol_prefix_combinations = S|`${unit_prefix}${S}` -export type code_to_extended_symbol = C extends null ? string : code_to_symbol[C]|`${unit_prefix}${code_to_symbol[C]}` +export type code_to_extended_symbol = C extends null ? string : symbol_prefix_combinations|UnitAliasesMap[C & keyof UnitAliasesMap]> export type symbol_with_prefix = unit_symbol|`${unit_prefix}${unit_symbol}` \ No newline at end of file diff --git a/types/quantity.ts b/types/quantity.ts index a0c359b2..006c5ee6 100644 --- a/types/quantity.ts +++ b/types/quantity.ts @@ -38,7 +38,7 @@ type expanded_symbol = [factor_num:number|bigint, factor_den:number|bigint, unit // Quantity with unit -export class Quantity { +export class Quantity { static cached_binaries = new Map(); @@ -102,7 +102,7 @@ export class Quantity { * @param value can be a number, bigint, or string: '1.25', '1', '0.5e12', '1/10', or [numerator, denominator] * @param unit */ - constructor(value?:number|bigint|string|[num:number|bigint, den:number|bigint], unit?:U extends Unit ? code_to_extended_symbol : unknown) + constructor(value?:number|bigint|string|[num:number|bigint, den:number|bigint], unit?:code_to_extended_symbol) constructor(value?:number|bigint|string|[num:number|bigint, den:number|bigint], encoded_unit?:unit) constructor(value:number|bigint|string|[num:number|bigint, den:number|bigint] = 1, symbol_or_encoded_unit:string|unit = 'x') { From bc62ff2eb66df02ba5bfbf98814a8df9c3b6f062 Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 22 Feb 2024 19:12:34 +0100 Subject: [PATCH 15/56] improve Time methods --- types/time.ts | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/types/time.ts b/types/time.ts index 4987c988..b0f2994f 100644 --- a/types/time.ts +++ b/types/time.ts @@ -1,4 +1,4 @@ -import { Unit } from "../compiler/unit_codes.ts"; +import { Unit, code_to_extended_symbol} from "../compiler/unit_codes.ts"; import { Quantity } from "./quantity.ts"; export class Time extends Date { @@ -14,8 +14,14 @@ export class Time extends Date { return `~${this.toISOString().replace("T"," ").replace("Z","")}~` } + plus(time:Quantity): Time + plus(amount: number, unit: code_to_extended_symbol): Time + plus(time:Quantity|number, unit?: code_to_extended_symbol) { + if (typeof time == "number") { + if (unit == undefined) throw new Error("unit is required") + else time = new Quantity(time, unit) + } - plus(time:Quantity) { if (time.hasBaseUnit('s')) { return new Time(this.getTime()+(time.value*1000)) } @@ -24,9 +30,19 @@ export class Time extends Date { new_time.add(time); return new_time } + else { + throw new Error("Invalid time unit") + } } - minus(time:Quantity) { + minus(time:Quantity): Time + minus(amount: number, unit: code_to_extended_symbol): Time + minus(time:Quantity|number, unit?: code_to_extended_symbol) { + if (typeof time == "number") { + if (unit == undefined) throw new Error("unit is required") + else time = new Quantity(time, unit) + } + if (time.hasBaseUnit('s')) { return new Time(this.getTime()-(time.value*1000)) } @@ -35,11 +51,21 @@ export class Time extends Date { new_time.subtract(time); return new_time } + else { + throw new Error("Invalid time unit") + } } - add(time:Quantity) { + add(time:Quantity): void + add(amount: number, unit: code_to_extended_symbol): void + add(time:Quantity|number, unit?: code_to_extended_symbol) { + if (typeof time == "number") { + if (unit == undefined) throw new Error("unit is required") + else time = new Quantity(time, unit) + } + if (time.hasBaseUnit('s')) { this.setTime(this.getTime()+(time.value*1000)) console.log(this.getTime(), time.value*1000, this.getTime()+(time.value*1000)) @@ -48,15 +74,28 @@ export class Time extends Date { else if (time.hasBaseUnit('Cmo')) { this.setMonth(this.getMonth()+time.value); } + else { + throw new Error("Invalid time unit") + } } - subtract(time:Quantity) { + subtract(time:Quantity): void + subtract(amount: number, unit: code_to_extended_symbol): void + subtract(time:Quantity|number, unit?: code_to_extended_symbol) { + if (typeof time == "number") { + if (unit == undefined) throw new Error("unit is required") + else time = new Quantity(time, unit) + } + if (time.hasBaseUnit('s')) { this.setTime(this.getTime()-(time.value*1000)) } else if (time.hasBaseUnit('Cmo')) { this.setMonth(this.getMonth()-time.value); } + else { + throw new Error("Invalid time unit") + } } } \ No newline at end of file From 424ef2f6385c60057c4ff5dd5958ff362769623c Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 23 Feb 2024 00:45:31 +0100 Subject: [PATCH 16/56] loading time improvements, don't load wasm per default --- compiler/compiler.ts | 4 +- network/unyt.ts | 6 +- runtime/runtime.ts | 52 +++++++--- storage/storage-locations/sql-db.ts | 153 ++++++++++++++++++++++++++-- storage/storage.ts | 73 ++++++++++--- types/type.ts | 3 + utils/local_files.ts | 4 +- utils/message_logger.ts | 12 ++- 8 files changed, 263 insertions(+), 44 deletions(-) diff --git a/compiler/compiler.ts b/compiler/compiler.ts index c233b072..f61a7c70 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -45,8 +45,8 @@ import { client_type } from "../utils/constants.ts"; import { normalizePath } from "../utils/normalize-path.ts"; import { VolatileMap } from "../utils/volatile-map.ts"; -await wasm_init(); -wasm_init_runtime(); +// await wasm_init(); +// wasm_init_runtime(); export const activePlugins:string[] = []; diff --git a/network/unyt.ts b/network/unyt.ts index 5dc43802..7336c270 100644 --- a/network/unyt.ts +++ b/network/unyt.ts @@ -127,7 +127,7 @@ export class Unyt { content += `${ESCAPE_SEQUENCES.UNYT_GREY}© ${new Date().getFullYear().toString()} unyt.org` logger.plain `#image(70,'unyt')${console_theme == "dark" ? this.logo_dark : this.logo_light} -Connected to the Supranet via ${info.node} ${info.interface ? `(${info.interface.interfaceProperties?.type}${info.interface.interfaceProperties?.name?` to ${ESCAPE_SEQUENCES.UNYT_GREY}${info.interface.interfaceProperties?.name}`:''}${ESCAPE_SEQUENCES.WHITE})` : ''} +Connected to the Supranet via ${ESCAPE_SEQUENCES.BOLD}${info.node?.toString()}${ESCAPE_SEQUENCES.RESET} ${info.interface ? `(${info.interface.interfaceProperties?.type}${info.interface.interfaceProperties?.name?` to ${ESCAPE_SEQUENCES.UNYT_GREY}${info.interface.interfaceProperties?.name}`:''}${ESCAPE_SEQUENCES.WHITE})` : ''} ${content} ` @@ -140,14 +140,14 @@ ${content} try { const alias = await endpoint?.getAlias(); if (alias) { - return `${alias} (${Runtime.valueToDatexStringExperimental(endpoint,false,true)}${ESCAPE_SEQUENCES.COLOR_DEFAULT})` + return `${ESCAPE_SEQUENCES.BOLD}${alias} (${endpoint}${ESCAPE_SEQUENCES.COLOR_DEFAULT})` } } catch { // ignore } // @@2134565, @endpoint - return Runtime.valueToDatexStringExperimental(endpoint,false,true); + return ESCAPE_SEQUENCES.BOLD + endpoint.toString() + ESCAPE_SEQUENCES.RESET; } diff --git a/runtime/runtime.ts b/runtime/runtime.ts index fb200fee..1e685f1e 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -32,7 +32,7 @@ import { BROADCAST, Endpoint, endpoints, IdEndpoint, LOCAL_ENDPOINT, Target, tar import { RuntimePerformance } from "./performance_measure.ts"; import { NetworkError, PermissionError, PointerError, RuntimeError, SecurityError, ValueError, Error as DatexError, CompilerError, TypeError, SyntaxError, AssertionError } from "../types/errors.ts"; import { Function as DatexFunction } from "../types/function.ts"; -import { Storage } from "../storage/storage.ts"; +import { MatchCondition, Storage } from "../storage/storage.ts"; import { Observers } from "../utils/observers.ts"; import { BinaryCode } from "../compiler/binary_codes.ts"; import type { ExecConditions, trace, compile_info, datex_meta, datex_scope, dxb_header, routing_info } from "../utils/global_types.ts"; @@ -585,9 +585,9 @@ export class Runtime { } // get content of https://, file://, ... - public static async getURLContent(url_string:string, raw?:RAW, cached?:boolean):Promise - public static async getURLContent(url:URL, raw?:RAW, cached?:boolean):Promise - public static async getURLContent(url_string:string|URL, raw:RAW=false, cached = false):Promise { + public static async getURLContent(url_string:string, raw?:RAW, cached?:boolean, potentialDatexAsJsModule?: boolean):Promise + public static async getURLContent(url:URL, raw?:RAW, cached?:boolean, potentialDatexAsJsModule?:boolean):Promise + public static async getURLContent(url_string:string|URL, raw:RAW=false, cached = false, potentialDatexAsJsModule = true):Promise { if (url_string.toString().startsWith("route:") && window.location?.origin) url_string = new URL(url_string.toString().replace("route:", ""), window.location.origin) @@ -609,11 +609,23 @@ export class Runtime { if (url.protocol == "https:" || url.protocol == "http:" || url.protocol == "blob:") { let response:Response|undefined = undefined; + let overrideContentType: string|undefined; let doFetch = true; - // possible js module import: fetch headers first and check content type: - if (!raw && (url_string.endsWith("js") || url_string.endsWith("ts") || url_string.endsWith("tsx") || url_string.endsWith("jsx") || url_string.endsWith("dx") || url_string.endsWith("dxb"))) { + + // exceptions to force potentialDatexAsJsModule (definitely dx files) + if (url_string.endsWith("/.dxb") || url_string.endsWith("/.dx") || url_string == "https://unyt.cc/nodes.dx") { + potentialDatexAsJsModule = false; + } + + // js module import + if (!raw && (url_string.endsWith("js") || url_string.endsWith("ts") || url_string.endsWith("tsx") || url_string.endsWith("jsx"))) { + doFetch = false; // no body fetch required, can directly import() module + overrideContentType = "application/javascript" + } + // potential js module as dxb/dx: fetch headers first and check content type + else if (!raw && potentialDatexAsJsModule && (url_string.endsWith("dx") || url_string.endsWith("dxb"))) { try { response = await fetch(url, {method: 'HEAD', cache: 'no-store'}); const type = response.headers.get('content-type'); @@ -641,33 +653,33 @@ export class Runtime { } } - const type = response.headers.get('content-type'); + const type = overrideContentType ?? response?.headers.get('content-type'); if (type == "application/datex" || type == "text/dxb" || url_string.endsWith(".dxb")) { - const content = await response.arrayBuffer(); + const content = await response!.arrayBuffer(); if (raw) result = [content, type]; else result = await this.executeDXBLocally(content, url); } else if (type?.startsWith("text/datex") || url_string.endsWith(".dx")) { - const content = await response.text() + const content = await response!.text() if (raw) result = [content, type]; else result = await this.executeDatexLocally(content, undefined, undefined, url); } else if (type?.startsWith("application/json5") || url_string.endsWith(".json5")) { - const content = await response.text(); + const content = await response!.text(); if (raw) result = [content, type]; else result = await Runtime.datexOut([content, [], {sign:false, encrypt:false, type:ProtocolDataType.DATA}]); } else if (type?.startsWith("application/json") || type?.endsWith("+json")) { - if (raw) result = [await response.text(), type]; - else result = await response.json() + if (raw) result = [await response!.text(), type]; + else result = await response!.json() } else if (type?.startsWith("text/javascript") || type?.startsWith("application/javascript")) { - if (raw) result = [await response.text(), type]; + if (raw) result = [await response!.text(), type]; else result = await import(url_string) } else { - const content = await response.arrayBuffer() + const content = await response!.arrayBuffer() if (raw) result = [content, type]; else { if (!type) throw Error("Cannot infer type from URL content"); @@ -2828,8 +2840,8 @@ export class Runtime { const compiled = new Uint8Array(Compiler.encodeValue(value, undefined, false, deep_clone, collapse_value, false, true, false, true)); return wasm_decompile(compiled, formatted, colorized, resolve_slots).replace(/\r\n$/, ''); } catch (e) { - console.log(e); - return "/* ERROR: Decompiler Error */"; + console.debug(e); + return this.valueToDatexString(value, formatted) } // return Decompiler.decompile(Compiler.encodeValue(value, undefined, false, deep_clone, collapse_value), true, formatted, formatted, false); } @@ -7351,6 +7363,14 @@ Type.std.time.setJSInterface({ }) +Type.std.MatchCondition.setJSInterface({ + class: MatchCondition, + visible_children: new Set([ + "type", + "data" + ]) +}) + Type.get("js:Function").setJSInterface({ diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 683e1089..ed1d9183 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -18,7 +18,7 @@ import { TypedArray } from "../../utils/global_values.ts"; import { MessageLogger } from "../../utils/message_logger.ts"; import { Join } from "https://deno.land/x/sql_builder@v1.9.2/join.ts"; import { LazyPointer } from "../../runtime/lazy-pointer.ts"; -import { MatchOptions } from "../storage.ts"; +import { MatchOptions, MatchCondition, MatchConditionType } from "../storage.ts"; import { MatchResult } from "../storage.ts"; import { Time } from "../../types/time.ts"; @@ -56,10 +56,24 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { rawPointers: { name: "__datex_pointers_raw", columns: [ - [this.#pointerMysqlColumnName, "varchar(50)", "PRIMARY KEY"], + [this.#pointerMysqlColumnName, this.#pointerMysqlType, "PRIMARY KEY"], ["value", "blob"] ] }, + sets: { + name: "__datex_sets", + columns: [ + [this.#pointerMysqlColumnName, this.#pointerMysqlType, "PRIMARY KEY"], + ["hash", "varchar(50)", "PRIMARY KEY"], + ["value_dxb", "blob"], + ["value_text", "text"], + ["value_integer", "int"], + ["value_decimal", "double"], + ["value_boolean", "boolean"], + ["value_time", "datetime"], + ["value_pointer", this.#pointerMysqlType] + ] + }, items: { name: "__datex_items", columns: [ @@ -191,9 +205,17 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #createTable(definition: TableDefinition) { + const compositePrimaryKeyColumns = definition.columns.filter(col => col[2]?.includes("PRIMARY KEY")); + if (compositePrimaryKeyColumns.length > 1) { + for (const col of compositePrimaryKeyColumns) { + col[2] = col[2]?.replace("PRIMARY KEY", "") + } + } + const primaryKeyDefinition = compositePrimaryKeyColumns.length > 1 ? `, PRIMARY KEY (${compositePrimaryKeyColumns.map(col => `\`${col[0]}\``).join(', ')})` : ''; + await this.#queryFirst(`CREATE TABLE ?? (${definition.columns.map(col => `\`${col[0]}\` ${col[1]} ${col[2]??''}` - ).join(', ')}${definition.constraints?.length ? ',' + definition.constraints.join(',') : ''});`, [definition.name]) + ).join(', ')}${definition.constraints?.length ? ',' + definition.constraints.join(',') : ''}${primaryKeyDefinition});`, [definition.name]) } /** @@ -239,11 +261,14 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { /** * Returns the table name for a given type. + * Converts UpperCamelCase to snake_case and pluralizes the name. * Does not validate if the table exists */ #typeToTableName(type: Datex.Type) { if (!type.template) throw new Error("Cannot create table for non-templated type " + type) - return type.namespace=="ext" ? type.name : `${type.namespace}_${type.name}`; + const snakeCaseName = type.name.replace(/([A-Z])/g, "_$1").toLowerCase().slice(1); + const snakeCasePlural = snakeCaseName + (snakeCaseName.endsWith("s") ? "es" : "s"); + return type.namespace=="ext" ? snakeCasePlural : `${type.namespace}_${snakeCasePlural}`; } #typeToString(type: Datex.Type) { @@ -292,8 +317,16 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { let foreignTable = await this.#getTableForType(propType); if (!foreignTable) { - logger.warn("Cannot map type " + propType + " to a SQL table, falling back to raw pointer storage") - foreignTable = this.#metaTables.rawPointers.name; + + // "set" table + if (propType.base_type == Type.std.Set) { + foreignTable = this.#metaTables.sets.name; + } + else { + logger.warn("Cannot map type " + propType + " to a SQL table, falling back to raw pointer storage") + foreignTable = this.#metaTables.rawPointers.name; + } + } columns.push([propName, this.#pointerMysqlType]) @@ -367,7 +400,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { /** * Insert a pointer into the database, pointer type must be templated */ - async #insertPointer(pointer: Datex.Pointer) { + async #insertTemplatedPointer(pointer: Datex.Pointer) { const table = await this.#getTableForType(pointer.type) if (!table) throw new Error("Cannot store pointer of type " + pointer.type + " in a custom table") const columns = await this.#getTableColumns(table); @@ -553,6 +586,50 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return dependencies; } + async #setPointerSet(pointer: Pointer) { + if (!(pointer.val instanceof Set)) throw new Error("Pointer value must be a Set"); + + const dependencies = new Set() + + const builder = new Query().table(this.#metaTables.sets.name) + const entries = [] + // add default entry (also for empty set) + entries.push({ + [this.#pointerMysqlColumnName]: pointer.id, + hash: "", + value_dxb: null, + value_text: null, + value_integer: null, + value_decimal: null, + value_boolean: null, + value_time: null, + value_pointer: null + }) + + for (const val of pointer.val) { + const hash = await Compiler.getValueHashString(val) + const data = {[this.#pointerMysqlColumnName]: pointer.id, hash} as Record; + const valPtr = Datex.Pointer.pointerifyValue(val); + + if (typeof val == "string") data.value_text = val + else if (typeof val == "number") data.value_integer = val + else if (typeof val == "boolean") data.value_boolean = val + else if (val instanceof Date) data.value_time = val + else if (valPtr instanceof Pointer) { + data.value_pointer = valPtr.id + dependencies.add(valPtr); + } + else data.value_dxb = Compiler.encodeValue(val, dependencies, true, false, true) + entries.push(data) + } + builder.insert(entries); + + const {result} = await this.#query(builder.build(), undefined, true) + // add to pointer mapping + if (result.affectedRows == 1) await this.#updatePointerMapping(pointer.id, this.#metaTables.sets.name) + return dependencies; + } + async #updatePointerMapping(pointerId: string, tableName: string) { await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE table_name=?;', [this.#metaTables.pointerMapping.name, [this.#pointerMysqlColumnName, "table_name"], [pointerId, tableName], tableName]) } @@ -660,6 +737,39 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { wheresOr.push(Where.expr(`${entryIdentifier!} REGEXP ?`, or.source)) } + // match condition + else if (or instanceof MatchCondition) { + if (!namespacedKey) throw new Error("missing namespacedKey"); + + if (or.type == MatchConditionType.BETWEEN) { + const condition = or as MatchCondition + wheresOr.push(Where.between(entryIdentifier!, condition.data[0], condition.data[1])) + } + else if (or.type == MatchConditionType.GREATER_THAN) { + const condition = or as MatchCondition + wheresOr.push(Where.gt(entryIdentifier!, condition.data)) + } + else if (or.type == MatchConditionType.LESS_THAN) { + const condition = or as MatchCondition + wheresOr.push(Where.lt(entryIdentifier!, condition.data)) + } + else if (or.type == MatchConditionType.GREATER_OR_EQUAL) { + const condition = or as MatchCondition + wheresOr.push(Where.gte(entryIdentifier!, condition.data)) + } + else if (or.type == MatchConditionType.LESS_OR_EQUAL) { + const condition = or as MatchCondition + wheresOr.push(Where.lte(entryIdentifier!, condition.data)) + } + else if (or.type == MatchConditionType.NOT_EQUAL) { + const condition = or as MatchCondition + wheresOr.push(Where.ne(entryIdentifier!, condition.data)) + } + else { + throw new Error("Unsupported match condition type " + or.type) + } + } + else if (typeof or == "object" && !(or == null || or instanceof Date)) { // is pointer @@ -800,7 +910,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // new full insert if (partialUpdateKey === NOT_EXISTING || !await this.hasPointer(pointer.id)) { - return this.#insertPointer(pointer) + return this.#insertTemplatedPointer(pointer) } // partial update else { @@ -824,6 +934,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } + // is set, store in set table + else if (pointer.type == Type.std.Set) { + return this.#setPointerSet(pointer) + } + // no template, just add a raw DXB entry, partial updates are not supported else { return this.#setPointerRaw(pointer) @@ -851,6 +966,28 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return value ? Runtime.decodeValue(this.#stringToBinary(value), outer_serialized) : NOT_EXISTING; } + // is set pointer + else if (table == this.#metaTables.sets.name) { + const values = await this.#query<{value_text:string, value_integer:number, value_boolean:boolean, value_time:Date, value_pointer:string, value_dxb:string}>( + new Query() + .table(this.#metaTables.sets.name) + .select("value_text", "value_integer", "value_boolean", "value_time", "value_pointer", "value_dxb") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .where(Where.ne("hash", "")) + .build() + ) + const result = new Set() + for (const {value_text, value_integer, value_boolean, value_time, value_pointer, value_dxb} of values) { + if (value_text !== undefined) result.add(value_text) + else if (value_integer !== undefined) result.add(value_integer) + else if (value_boolean !== undefined) result.add(value_boolean) + else if (value_time !== undefined) result.add(value_time) + else if (value_pointer !== undefined) result.add(await Pointer.load(value_pointer)) + else if (value_dxb !== undefined) result.add(await Runtime.decodeValue(this.#stringToBinary(value_dxb))) + } + return result; + } + // is templated pointer else { const type = await this.#getTypeForTable(table); diff --git a/storage/storage.ts b/storage/storage.ts index 1ea44f33..14801541 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -18,6 +18,7 @@ import { StorageSet } from "../types/storage-set.ts"; import { IterableWeakSet } from "../utils/iterable-weak-set.ts"; import { LazyPointer } from "../runtime/lazy-pointer.ts"; import { AutoMap } from "../utils/auto_map.ts"; +import { JSInterface } from "../runtime/js_interface.ts"; // displayInit(); @@ -39,18 +40,22 @@ export const comparatorKeys = [ "=", "!=", ">", ">=", "<", "<=" ] as const -type AtomicMatchInput = T | +type AtomicMatchInput = T | ( T extends string ? RegExp : never ) -type _MatchInput = T extends object ? - { - [K in keyof T]?: MatchInputValue - } : - AtomicMatchInput|AtomicMatchInput[] +type _MatchInput = + MatchCondition | + ( + T extends object ? + { + [K in keyof T]?: MatchInputValue + } : + AtomicMatchInput|AtomicMatchInput[] + ) type MatchInputValue = _MatchInput| // exact match _MatchInput[]| // or match @@ -58,7 +63,16 @@ type MatchInputValue = export type MatchInput = MatchInputValue -export type MatchOptions = { +type ObjectKeyPaths = + T extends object ? + ( + ObjectKeyPaths extends never ? + `${string & Exclude}` : + `${string & Exclude}`|`${string & Exclude}.${ObjectKeyPaths]>}` + ): + never + +export type MatchOptions = { /** * Maximum number of matches to return */ @@ -66,7 +80,7 @@ export type MatchOptions = { /** * Sort by key (e.g. address.street) */ - sortBy?: string, + sortBy?: string // TODO: T extends object ? ObjectKeyPaths : string, /** * Sort in descending order (only if sortBy is set) */ @@ -101,6 +115,41 @@ export type AdvancedMatchResult = { matches: Set } +export enum MatchConditionType { + BETWEEN = "BETWEEN", + LESS_THAN = "LESS_THAN", + GREATER_THAN = "GREATER_THAN", + LESS_OR_EQUAL = "LESS_OR_EQUAL", + GREATER_OR_EQUAL = "GREATER_OR_EQUAL", + NOT_EQUAL = "NOT_EQUAL" +} +export type MatchConditionData = + T extends MatchConditionType.BETWEEN ? + [V, V] : + T extends MatchConditionType.LESS_THAN|MatchConditionType.GREATER_THAN|MatchConditionType.LESS_OR_EQUAL|MatchConditionType.GREATER_OR_EQUAL|MatchConditionType.NOT_EQUAL ? + V : + never + +export class MatchCondition { + + constructor( + public type: Type, + public data: MatchConditionData + ) {} + + static between(lower: V, upper: V) { + return new MatchCondition(MatchConditionType.BETWEEN, [lower, upper]) + } + + static lessThan(value: V) { + return new MatchCondition(MatchConditionType.LESS_THAN, value) + } + + static greaterThan(value: V) { + return new MatchCondition(MatchConditionType.GREATER_THAN, value) + } +} + export interface StorageLocation { name: string @@ -140,7 +189,7 @@ export interface StorageLocation|void supportsMatchForType?(type: Type): Promise|boolean - matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): Promise>|MatchResult + matchQuery?>(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): Promise>|MatchResult clear(): Promise|void } @@ -171,7 +220,7 @@ export abstract class SyncStorageLocation implements StorageLocation(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): MatchResult + matchQuery?>(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): MatchResult abstract clear(): void } @@ -202,7 +251,7 @@ export abstract class AsyncStorageLocation implements StorageLocation supportsMatchForType?(type: Type): Promise|boolean - matchQuery?(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): Promise> + matchQuery?>(itemPrefix: string, valueType: Type, match: MatchInput, options:Options): Promise> abstract clear(): Promise } @@ -1014,7 +1063,7 @@ export class Storage { return (this.#primary_location?.supportsMatchSelection && await this.#primary_location?.supportsMatchForType!(type)) ?? false; } - public static itemMatchQuery(itemPrefix: string, valueType:Type, match: MatchInput, options?:Options) { + public static itemMatchQuery>(itemPrefix: string, valueType:Type, match: MatchInput, options?:Options) { options ??= {} as Options; if (!this.#primary_location?.supportsMatchSelection) throw new Error("Primary storage location does not support match queries"); return this.#primary_location!.matchQuery!(itemPrefix, valueType, match, options); diff --git a/types/type.ts b/types/type.ts index 91dccbed..35edd8a3 100644 --- a/types/type.ts +++ b/types/type.ts @@ -23,6 +23,7 @@ import {StorageMap, StorageWeakMap} from "./storage-map.ts" import {StorageSet, StorageWeakSet} from "./storage-set.ts" import { ExtensibleFunction } from "./function-utils.ts"; import type { JSTransferableFunction } from "./js-function.ts"; +import type { MatchCondition } from "../storage/storage.ts"; export type inferDatexType = T extends Type ? JST : any; @@ -1025,6 +1026,8 @@ export class Type extends ExtensibleFunction { Assertion: Type.get("std:Assertion"), Iterator: Type.get>("std:Iterator"), + MatchCondition: Type.get("std:MatchCondition"), + StorageMap: Type.get>("std:StorageMap"), StorageWeakMap: Type.get>("std:StorageWeakMap"), StorageSet: Type.get>("std:StorageSet"), diff --git a/utils/local_files.ts b/utils/local_files.ts index 32dbf32e..f3523110 100644 --- a/utils/local_files.ts +++ b/utils/local_files.ts @@ -1,6 +1,6 @@ import { DATEX_FILE_TYPE, FILE_TYPE } from "../compiler/compiler.ts"; import { Runtime } from "../runtime/runtime.ts"; -import { decompile } from "../wasm/adapter/pkg/datex_wasm.js"; +import wasm_init, { decompile } from "../wasm/adapter/pkg/datex_wasm.js"; export async function uploadDatexFile(){ const pickerOpts = { @@ -30,6 +30,8 @@ export async function uploadDatexFile(){ export type datex_file_data = {type: DATEX_FILE_TYPE, text:string, binary?:ArrayBuffer, fileHandle:any}; export async function getDatexContentFromFileHandle(fileHandle:any) { + // init wasm + await wasm_init(); const fileData = await fileHandle.getFile(); const data:datex_file_data = { diff --git a/utils/message_logger.ts b/utils/message_logger.ts index dbb0650e..9a057265 100644 --- a/utils/message_logger.ts +++ b/utils/message_logger.ts @@ -5,7 +5,7 @@ import { Logical } from "../types/logic.ts"; import { Logger } from "./logger.ts"; // WASM -import {decompile as wasm_decompile} from "../wasm/adapter/pkg/datex_wasm.js"; +import wasm_init, {decompile as wasm_decompile} from "../wasm/adapter/pkg/datex_wasm.js"; import { console } from "./ansi_compat.ts"; import { ESCAPE_SEQUENCES } from "./logger.ts"; @@ -14,6 +14,7 @@ export class MessageLogger { static logger:Logger static decompile(dxb:ArrayBuffer, has_header = true, colorized = true, resolve_slots = true){ + if (!this.#initialized) return "[DATEX Decompiler not enabled]" try { // extract body (TODO: just temporary, rust impl does not yet support header decompilation) if (has_header) { @@ -28,7 +29,14 @@ export class MessageLogger { } } - static enable(showRedirectedMessages = true){ + static #initialized = false; + static async init() { + await wasm_init() + this.#initialized = true; + } + + static async enable(showRedirectedMessages = true){ + await this.init(); IOHandler.resetDatexHandlers(); if (!this.logger) this.logger = new Logger("DATEX Message"); From 0561bff44a3a10aa44ff791b1b99eea673cdbc9b Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 24 Feb 2024 13:07:23 +0100 Subject: [PATCH 17/56] fix isArrowFunction --- types/function-utils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/types/function-utils.ts b/types/function-utils.ts index 1c5d14ab..c3ccd20b 100644 --- a/types/function-utils.ts +++ b/types/function-utils.ts @@ -121,6 +121,7 @@ export function getSourceWithoutUsingDeclaration(fn: (...args:unknown[])=>unknow if (fnSource.startsWith("async")) fnSource = fnSource.replace("async", "async function") else fnSource = "function " + fnSource } + return fnSource .replace(/(?<=(?:(?:[\w\s*])+\(.*\)\s*{|\(.*\)\s*=>\s*{?|.*\s*=>\s*{?)\s*)(use\s*\((?:[\s\S]*?)\))/, 'true /*$1*/') } @@ -132,7 +133,7 @@ const isNormalFunction = (fnSrc:string) => { return !!fnSrc.match(/^(async\s+)?function(\(| |\*)/) } const isArrowFunction = (fnSrc:string) => { - return !!fnSrc.match(/^(async\s+)?\([^)]*\)\s*=>/) + return !!fnSrc.match(/^(async\s+)?(\([^)]*\)|\w+)\s*=>/) } const isNativeFunction = (fnSrc:string) => { From 09c64868996b8f447ee2e3568116b3950ad86a29 Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 24 Feb 2024 13:07:54 +0100 Subject: [PATCH 18/56] unrelated changes --- datex_all.ts | 4 ++-- js_adapter/js_class_adapter.ts | 2 +- js_adapter/legacy_decorators.ts | 2 +- network/communication-hub.ts | 2 +- network/communication-interface.ts | 2 +- runtime/js_interface.ts | 2 +- runtime/pointers.ts | 2 +- runtime/runtime.ts | 2 +- storage/storage-locations/deno-kv.ts | 2 +- storage/storage-locations/indexed-db.ts | 2 +- storage/storage-locations/sql-db.ts | 27 ++++++++++++++----------- threads/promise-fn-types.ts | 2 +- threads/threads.ts | 2 +- types/addressing.ts | 2 +- types/assertion.ts | 2 +- types/function.ts | 2 +- types/iterator.ts | 2 +- types/storage-set.ts | 2 +- types/stream.ts | 2 +- utils/match.ts | 2 +- 20 files changed, 35 insertions(+), 32 deletions(-) diff --git a/datex_all.ts b/datex_all.ts index fc4428d0..d9906377 100644 --- a/datex_all.ts +++ b/datex_all.ts @@ -7,7 +7,7 @@ export * from "./js_adapter/js_class_adapter.ts"; export * from "./js_adapter/legacy_decorators.ts"; // utils -export * from "./utils/global_types.ts"; +export type * from "./utils/global_types.ts"; export * from "./utils/global_values.ts"; export * from "./utils/logger.ts"; export * from "./utils/observers.ts"; @@ -42,7 +42,7 @@ export * from "./runtime/cache_path.ts"; export * from "./storage/storage.ts"; // types -export * from "./types/abstract_types.ts"; +export type * from "./types/abstract_types.ts"; export * from "./types/addressing.ts"; export * from "./types/assertion.ts"; export * from "./types/logic.ts"; diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index 9cdc44f9..ae5ae65f 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -24,7 +24,7 @@ import { Function as DatexFunction } from "../types/function.ts"; import { DatexObject } from "../types/object.ts"; import { Tuple } from "../types/tuple.ts"; import { DX_PERMISSIONS, DX_TYPE, DX_ROOT, INIT_PROPS, DX_EXTERNAL_SCOPE_NAME, DX_EXTERNAL_FUNCTION_NAME } from "../runtime/constants.ts"; -import { type Class } from "../utils/global_types.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"; diff --git a/js_adapter/legacy_decorators.ts b/js_adapter/legacy_decorators.ts index 18f7b847..cc75aafa 100644 --- a/js_adapter/legacy_decorators.ts +++ b/js_adapter/legacy_decorators.ts @@ -15,7 +15,7 @@ import { } from "../runtime/runtime.ts"; import { endpoint_name, Target, target_clause } from "../types/addressing.ts"; import { Type } from "../types/type.ts"; import { UpdateScheduler, Pointer } from "../runtime/pointers.ts"; -import { Class } from "../utils/global_types.ts"; +import type { Class } from "../utils/global_types.ts"; // decorator types export type context_kind = 'class'|'method'|'getter'|'setter'|'field'|'auto-accessor'; diff --git a/network/communication-hub.ts b/network/communication-hub.ts index e9b0b703..5b743094 100644 --- a/network/communication-hub.ts +++ b/network/communication-hub.ts @@ -1,4 +1,4 @@ -import { dxb_header } from "../utils/global_types.ts"; +import type { dxb_header } from "../utils/global_types.ts"; import { Endpoint, BROADCAST, LOCAL_ENDPOINT } from "../types/addressing.ts"; import { CommunicationInterface, CommunicationInterfaceSocket, ConnectedCommunicationInterfaceSocket } from "./communication-interface.ts"; import { Disjunction } from "../types/logic.ts"; diff --git a/network/communication-interface.ts b/network/communication-interface.ts index 266175b9..c16adb3c 100644 --- a/network/communication-interface.ts +++ b/network/communication-interface.ts @@ -6,7 +6,7 @@ import { COM_HUB_SECRET, communicationHub } from "./communication-hub.ts"; import { IOHandler } from "../runtime/io_handler.ts"; import { LOCAL_ENDPOINT } from "../types/addressing.ts"; import { Runtime } from "../runtime/runtime.ts"; -import { dxb_header } from "../utils/global_types.ts"; +import type { dxb_header } from "../utils/global_types.ts"; export enum InterfaceDirection { /** diff --git a/runtime/js_interface.ts b/runtime/js_interface.ts index e4b838df..7011ab5f 100644 --- a/runtime/js_interface.ts +++ b/runtime/js_interface.ts @@ -1,6 +1,6 @@ import { Type } from "../types/type.ts"; import { Endpoint } from "../types/addressing.ts"; -import { fundamental } from "../types/abstract_types.ts"; +import type { fundamental } from "../types/abstract_types.ts"; import type { Class } from "../utils/global_types.ts"; import { Pointer } from "./pointers.ts"; import { INVALID, NOT_EXISTING } from "./constants.ts"; diff --git a/runtime/pointers.ts b/runtime/pointers.ts index de7d63c0..a583463a 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -11,7 +11,7 @@ import { BinaryCode } from "../compiler/binary_codes.ts"; import { JSInterface } from "./js_interface.ts"; import { Stream } from "../types/stream.ts"; import { Tuple } from "../types/tuple.ts"; -import { primitive } from "../types/abstract_types.ts"; +import type { primitive } from "../types/abstract_types.ts"; import { Function as DatexFunction } from "../types/function.ts"; import { Quantity } from "../types/quantity.ts"; import { buffer2hex, hex2buffer } from "../utils/utils.ts"; diff --git a/runtime/runtime.ts b/runtime/runtime.ts index 1e685f1e..58e3d2f6 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -52,7 +52,7 @@ import { JSInterface } from "./js_interface.ts"; import { Stream } from "../types/stream.ts"; import { Quantity } from "../types/quantity.ts"; import { Scope } from "../types/scope.ts"; -import { fundamental } from "../types/abstract_types.ts"; +import type { fundamental } from "../types/abstract_types.ts"; import { IterationFunction as IteratorFunction, Iterator, RangeIterator } from "../types/iterator.ts"; import { Assertion } from "../types/assertion.ts"; import { Deferred } from "../types/deferred.ts"; diff --git a/storage/storage-locations/deno-kv.ts b/storage/storage-locations/deno-kv.ts index 615d0eee..a75ad9c6 100644 --- a/storage/storage-locations/deno-kv.ts +++ b/storage/storage-locations/deno-kv.ts @@ -7,7 +7,7 @@ import { AsyncStorageLocation } from "../storage.ts"; import { ptr_cache_path } from "../../runtime/cache_path.ts"; import { client_type } from "../../utils/constants.ts"; import { normalizePath } from "../../utils/normalize-path.ts"; -import { ExecConditions } from "../../utils/global_types.ts"; +import type { ExecConditions } from "../../utils/global_types.ts"; const denoKvDir = new URL("./deno-kv/", ptr_cache_path); // @ts-ignore global Deno diff --git a/storage/storage-locations/indexed-db.ts b/storage/storage-locations/indexed-db.ts index e4cbe1c3..aaddaf81 100644 --- a/storage/storage-locations/indexed-db.ts +++ b/storage/storage-locations/indexed-db.ts @@ -6,7 +6,7 @@ import { NOT_EXISTING } from "../../runtime/constants.ts"; import { AsyncStorageLocation, site_suffix } from "../storage.ts"; import localforage from "../../lib/localforage/localforage.js"; -import { ExecConditions } from "../../utils/global_types.ts"; +import type { ExecConditions } from "../../utils/global_types.ts"; // db based storage for DATEX value caching (IndexDB in the browser) const datex_item_storage = localforage.createInstance({name: "dxitem::"+site_suffix}); diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index ed1d9183..af036516 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -10,7 +10,7 @@ import { datex_type_mysql_map } from "./sql-type-map.ts"; import { NOT_EXISTING } from "../../runtime/constants.ts"; import { client_type } from "../../utils/constants.ts"; import { Compiler } from "../../compiler/compiler.ts"; -import { ExecConditions } from "../../utils/global_types.ts"; +import type { ExecConditions } from "../../utils/global_types.ts"; import { Runtime } from "../../runtime/runtime.ts"; import { Storage } from "../storage.ts"; import { Type } from "../../types/type.ts"; @@ -580,9 +580,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async #setPointerRaw(pointer: Pointer) { const dependencies = new Set() const encoded = Compiler.encodeValue(pointer, dependencies, true, false, true); - const {result} = await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [this.#metaTables.rawPointers.name, [this.#pointerMysqlColumnName, "value"], [pointer.id, encoded], encoded], true) - // add to pointer mapping - if (result.affectedRows == 1) await this.#updatePointerMapping(pointer.id, this.#metaTables.rawPointers.name) + await this.#setPointerInRawTable(pointer.id, encoded); return dependencies; } @@ -624,9 +622,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } builder.insert(entries); - const {result} = await this.#query(builder.build(), undefined, true) - // add to pointer mapping - if (result.affectedRows == 1) await this.#updatePointerMapping(pointer.id, this.#metaTables.sets.name) + // replace INSERT with INSERT IGNORE to prevent duplicate key errors + const {result} = await this.#query(builder.build().replace("INSERT", "INSERT IGNORE"), undefined, true) + // add to pointer mapping TODO: better decision if to add to pointer mapping + if (result.affectedRows) await this.#updatePointerMapping(pointer.id, this.#metaTables.sets.name) return dependencies; } @@ -950,7 +949,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // get table where pointer is stored const table = await this.#getPointerTable(pointerId); if (!table) { - logger.error("No table found for pointer " + pointerId); + console.warn("No table found for pointer " + pointerId); return NOT_EXISTING; } @@ -1071,16 +1070,20 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // check if raw pointer, otherwise not yet supported const table = await this.#getPointerTable(pointerId); if (table == this.#metaTables.rawPointers.name) { - const {result} = await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [table, [this.#pointerMysqlColumnName, "value"], [pointerId, value], value], true) - console.log("affectedRows", result.affectedRows) - // is newly inserted, add to pointer mapping - if (result.affectedRows == 1) await this.#updatePointerMapping(pointerId, this.#metaTables.rawPointers.name) + await this.#setPointerInRawTable(pointerId, value); } else { logger.error("Setting raw dxb value for templated pointer is not yet supported in SQL storage (pointer: " + pointerId + ", table: " + table + ")"); } } + async #setPointerInRawTable(pointerId: string, encoded: ArrayBuffer) { + const table = this.#metaTables.rawPointers.name; + const {result} = await this.#query('INSERT INTO ?? ?? VALUES ? ON DUPLICATE KEY UPDATE value=?;', [table, [this.#pointerMysqlColumnName, "value"], [pointerId, encoded], encoded], true) + // is newly inserted, add to pointer mapping + if (result.affectedRows == 1) await this.#updatePointerMapping(pointerId, table) + } + async hasPointer(pointerId: string): Promise { if (this.#existingPointersCache.has(pointerId)) return true; const count = (await this.#queryFirst<{COUNT: number}>( diff --git a/threads/promise-fn-types.ts b/threads/promise-fn-types.ts index eeadb5ff..3be11d94 100644 --- a/threads/promise-fn-types.ts +++ b/threads/promise-fn-types.ts @@ -3,7 +3,7 @@ * Promise methods return type inference magic */ -import { Equals } from "../utils/global_types.ts"; +import type { Equals } from "../utils/global_types.ts"; class _PromiseWrapper { all(e: T[]) { diff --git a/threads/threads.ts b/threads/threads.ts index e512b4e7..fd6eb2b4 100644 --- a/threads/threads.ts +++ b/threads/threads.ts @@ -1,5 +1,5 @@ import { Logger, console_theme } from "../utils/logger.ts"; -import { Equals } from "../utils/global_types.ts"; +import type { Equals } from "../utils/global_types.ts"; const logger = new Logger("thread-runner"); diff --git a/types/addressing.ts b/types/addressing.ts index dafcd767..136ed104 100644 --- a/types/addressing.ts +++ b/types/addressing.ts @@ -1,6 +1,6 @@ import { BinaryCode } from "../compiler/binary_codes.ts"; import { Pointer } from "../runtime/pointers.ts"; -import { ValueConsumer } from "./abstract_types.ts"; +import type { ValueConsumer } from "./abstract_types.ts"; import { ValueError } from "./errors.ts"; import { Compiler, ProtocolDataTypesMap } from "../compiler/compiler.ts"; import type { datex_scope, dxb_header, trace } from "../utils/global_types.ts"; diff --git a/types/assertion.ts b/types/assertion.ts index db01e5cf..716f4c7b 100644 --- a/types/assertion.ts +++ b/types/assertion.ts @@ -1,6 +1,6 @@ import { VOID } from "../runtime/constants.ts"; import type { datex_scope } from "../utils/global_types.ts"; -import { ValueConsumer } from "./abstract_types.ts"; +import type { ValueConsumer } from "./abstract_types.ts"; import { AssertionError, RuntimeError, ValueError } from "./errors.ts"; import { ExtensibleFunction } from "./function-utils.ts"; import { Callable } from "./function.ts"; diff --git a/types/function.ts b/types/function.ts index aba7ceef..ca909f04 100644 --- a/types/function.ts +++ b/types/function.ts @@ -1,7 +1,7 @@ import { Pointer, Ref } from "../runtime/pointers.ts"; import { Runtime } from "../runtime/runtime.ts"; import { logger } from "../utils/global_values.ts"; -import { StreamConsumer, ValueConsumer } from "./abstract_types.ts"; +import type { StreamConsumer, ValueConsumer } from "./abstract_types.ts"; import { BROADCAST, Endpoint, endpoint_name, LOCAL_ENDPOINT, target_clause } from "./addressing.ts"; import { Markdown } from "./markdown.ts"; import { Scope } from "./scope.ts"; diff --git a/types/iterator.ts b/types/iterator.ts index d26402bf..d44bc2ae 100644 --- a/types/iterator.ts +++ b/types/iterator.ts @@ -1,6 +1,6 @@ import { Tuple } from "./tuple.ts"; import type { datex_scope } from "../utils/global_types.ts"; -import { ValueConsumer } from "./abstract_types.ts"; +import type { ValueConsumer } from "./abstract_types.ts"; export class Iterator { diff --git a/types/storage-set.ts b/types/storage-set.ts index 724da55b..0a8a8673 100644 --- a/types/storage-set.ts +++ b/types/storage-set.ts @@ -6,7 +6,7 @@ import { Pointer } from "../runtime/pointers.ts"; import { MatchResult, Storage } from "../storage/storage.ts"; import { Logger } from "../utils/logger.ts"; import { MatchInput, match } from "../utils/match.ts"; -import { Class } from "../utils/global_types.ts"; +import type { Class } from "../utils/global_types.ts"; import { MatchOptions } from "../utils/match.ts"; import { Type } from "./type.ts"; diff --git a/types/stream.ts b/types/stream.ts index 942f40e6..44743a5d 100644 --- a/types/stream.ts +++ b/types/stream.ts @@ -1,7 +1,7 @@ import { ReadableStream } from "../runtime/runtime.ts"; import { Pointer } from "../runtime/pointers.ts"; import type { datex_scope } from "../utils/global_types.ts"; -import { StreamConsumer } from "./abstract_types.ts"; +import type { StreamConsumer } from "./abstract_types.ts"; import { Logger } from "../utils/logger.ts"; const logger = new Logger("Stream") diff --git a/utils/match.ts b/utils/match.ts index 3ff77357..0bc33617 100644 --- a/utils/match.ts +++ b/utils/match.ts @@ -1,6 +1,6 @@ import { StorageSet } from "../types/storage_set.ts"; import { Type } from "../types/type.ts"; -import { Class } from "./global_types.ts"; +import type { Class } from "./global_types.ts"; import { MatchInput, MatchResult, MatchOptions, Storage, comparatorKeys } from "../storage/storage.ts"; export type { MatchInput, MatchOptions, MatchResult } from "../storage/storage.ts"; From dee607828812b877f75b11dabdff70513931d396 Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 24 Feb 2024 20:19:56 +0100 Subject: [PATCH 19/56] improve matching (sql) --- storage/storage-locations/sql-db.ts | 283 ++++++++++++++++++++++------ storage/storage.ts | 45 ++++- utils/match.ts | 88 +-------- 3 files changed, 262 insertions(+), 154 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index af036516..ca9d9f35 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -18,9 +18,10 @@ import { TypedArray } from "../../utils/global_values.ts"; import { MessageLogger } from "../../utils/message_logger.ts"; import { Join } from "https://deno.land/x/sql_builder@v1.9.2/join.ts"; import { LazyPointer } from "../../runtime/lazy-pointer.ts"; -import { MatchOptions, MatchCondition, MatchConditionType } from "../storage.ts"; +import { MatchOptions, MatchCondition, MatchConditionType, MatchComputedProperty, MatchComputedPropertyType} from "../storage.ts"; import { MatchResult } from "../storage.ts"; import { Time } from "../../types/time.ts"; +import { Order } from "https://deno.land/x/sql_builder@v1.9.2/order.ts"; const logger = new Logger("SQL Storage"); @@ -143,7 +144,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } - async #query(query_string:string, query_params?:any[], returnRawResult: true): Promise<{rows:row[], result:ExecuteResult}> + async #query(query_string:string, query_params:any[]|undefined, returnRawResult: true): Promise<{rows:row[], result:ExecuteResult}> async #query(query_string:string, query_params?:any[]): Promise async #query(query_string:string, query_params?:any[], returnRawResult?: boolean): Promise { // prevent infinite recursion if calling query from within init() @@ -224,10 +225,14 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { * @returns */ async #getTableForType(type: Datex.Type) { - // type does not have a template, use raw pointer table if (!type.template) return null + // already has a table + const tableName = this.#typeToTableName(type); + if (this.#tableTypes.has(tableName)) return tableName; + + const existingTable = (await this.#queryFirst<{table_name: string}|undefined>( new Query() .table(this.#metaTables.typeMapping.name) @@ -253,7 +258,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (!type) { logger.error("No type found for table " + table); } - this.#tableTypes.set(table, type ? Datex.Type.get(type.type) : null) + else this.#tableTypes.set(table, Datex.Type.get(type.type)); } return this.#tableTypes.get(table) @@ -263,10 +268,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { * Returns the table name for a given type. * Converts UpperCamelCase to snake_case and pluralizes the name. * Does not validate if the table exists + * Throws if the type is not templated */ #typeToTableName(type: Datex.Type) { if (!type.template) throw new Error("Cannot create table for non-templated type " + type) - const snakeCaseName = type.name.replace(/([A-Z])/g, "_$1").toLowerCase().slice(1); + const snakeCaseName = type.name.replace(/([A-Z])/g, "_$1").toLowerCase().slice(1).replace(/__+/g, '_'); const snakeCasePlural = snakeCaseName + (snakeCaseName.endsWith("s") ? "es" : "s"); return type.namespace=="ext" ? snakeCasePlural : `${type.namespace}_${snakeCasePlural}`; } @@ -528,13 +534,9 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // const foreignPointerPlaceholders = await Promise.all(foreignPointerPlaceholderPromises) - console.log("foreignPointerPlaceholders", foreignPointerPlaceholders) - const objectString = Datex.Runtime.valueToDatexStringExperimental(object, false, false) .replace(/"\u0001(\d+)"/g, (_, index) => foreignPointerPlaceholders[parseInt(index)]||"error: no placeholder") - console.log("objectString", objectString) - return `${type.toString()} ${objectString}` } @@ -610,7 +612,8 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const valPtr = Datex.Pointer.pointerifyValue(val); if (typeof val == "string") data.value_text = val - else if (typeof val == "number") data.value_integer = val + else if (typeof val == "number") data.value_decimal = val + else if (typeof val == "bigint") data.value_boolean = val else if (typeof val == "boolean") data.value_boolean = val else if (val instanceof Date) data.value_time = val else if (valPtr instanceof Pointer) { @@ -648,33 +651,90 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async matchQuery(itemPrefix: string, valueType: Datex.Type, match: Datex.MatchInput, options: Options): Promise> { + const joins = new Map() + const collectedTableTypes = new Set([valueType]) + const collectedIdentifiers = new Set() const builder = new Query() .table(this.#metaTables.items.name) - // .select(this.#pointerMysqlColumnName) - .select(`SQL_CALC_FOUND_ROWS ${this.#typeToTableName(valueType)}.${this.#pointerMysqlColumnName} as ptrId`) - .where(Where.like("key", itemPrefix + "%")) + .where(Where.like(this.#metaTables.items.name + ".key", itemPrefix + "%")) .join( Join.left(this.#typeToTableName(valueType)).on(`${this.#metaTables.items.name}.${this.#pointerMysqlColumnName}`, `${this.#typeToTableName(valueType)}.${this.#pointerMysqlColumnName}`) ) + const where = this.buildQueryConditions(builder, match, joins, collectedTableTypes, collectedIdentifiers, valueType, undefined, undefined, options.computedProperties) + let query = "error"; - // limit, do limit later if options.returnPointerIds - if (options && (options.limit !== undefined && isFinite(options.limit) && !options.returnPointerIds)) { - builder.limit(options.offset ?? 0, options.limit) + const rootTableName = this.#typeToTableName(valueType); + + // computed properties - nested select + if (options.computedProperties) { + const select = [...collectedIdentifiers, this.#pointerMysqlColumnName].map(identifier => { + if (identifier.includes("__")) { + return `${this.getTableProperty(identifier)} as ${identifier}` + } + else return rootTableName + '.' + identifier; + }); + + for (const [name, value] of Object.entries(options.computedProperties)) { + if (value.type == MatchComputedPropertyType.GEOGRAPHIC_DISTANCE) { + const {pointA, pointB} = value.data; + const mockObject = {} + for (const property of [pointA.lat, pointA.lon, pointB.lat, pointB.lon]) { + // is property, not literal position + if (typeof property == "string") { + let object:Record = mockObject; + let lastParent:Record = mockObject; + let lastProperty: string|undefined + for (const part of property.split(".")) { + if (!object[part]) object[part] = {}; + lastParent = object; + lastProperty = part; + object = object[part]; + } + if (lastParent && lastProperty!=undefined) lastParent[lastProperty] = null; + } + } + // get correct joins + this.buildQueryConditions(builder, mockObject, joins, collectedTableTypes, new Set(), valueType) + + select.push( + `ST_Distance_Sphere(point(${ + typeof pointA.lon == "string" ? this.formatProperty(pointA.lon) : pointA.lon + },${ + typeof pointA.lat == "string" ? this.formatProperty(pointA.lat) : pointA.lat + }), point(${ + typeof pointB.lon == "string" ? this.formatProperty(pointB.lon) : pointB.lon + },${ + typeof pointB.lat == "string" ? this.formatProperty(pointB.lat) : pointB.lat + })) as ${name}` + ) + } + } + builder.select(...select); + joins.forEach(join => builder.join(join)); + + const outerBuilder = new Query() + .select(`DISTINCT SQL_CALC_FOUND_ROWS ${this.#pointerMysqlColumnName} as ptrId`) + .table('__placeholder__'); + + this.appendBuilderConditions(outerBuilder, options, where) + // nested select + query = outerBuilder.build().replace('`__placeholder__`', `(${builder.build()}) as _inner_res`) + } + + // no computed properties + else { + builder.select(`DISTINCT SQL_CALC_FOUND_ROWS ${this.#typeToTableName(valueType)}.${this.#pointerMysqlColumnName} as ptrId`); + this.appendBuilderConditions(builder, options, where) + joins.forEach(join => builder.join(join)); + query = builder.build(); } - - const joins = new Map() - const collectedTableTypes = new Set([valueType]) - const where = this.buildQueryConditions(builder, match, joins, collectedTableTypes, valueType) - if (where) builder.where(where); - joins.forEach(join => builder.join(join)); - // make sure all tables are created for (const type of collectedTableTypes) { await this.#getTableForType(type) } - const ptrIds = (await this.#query<{ptrId:string}>(builder.build())).map(({ptrId}) => ptrId) + const ptrIds = (await this.#query<{ptrId:string}>(query)).map(({ptrId}) => ptrId) const limitedPtrIds = options.returnPointerIds ? // offset and limit manually after query ptrIds.slice(options.offset ?? 0, options.limit ? (options.offset ?? 0) + options.limit : undefined) : @@ -683,7 +743,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // TODO: atomic operations for multiple queries const {foundRows} = await this.#queryFirst<{foundRows: number}>("SELECT FOUND_ROWS() as foundRows") ?? {foundRows: -1} - console.log("foundRows", foundRows) const result = new Set((await Promise.all(limitedPtrIds.map(ptrId => Pointer.load(ptrId)))).filter(ptr => { if (ptr instanceof LazyPointer) { @@ -705,11 +764,41 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - private buildQueryConditions(builder: Query, match: unknown, joins: Map, collectedTableTypes:Set, valueType:Type, namespacedKey?: string, previousKey?: string): Where|undefined { + private appendBuilderConditions(builder: Query, options: MatchOptions, where?: Where) { + // limit, do limit later if options.returnPointerIds + if (options && (options.limit !== undefined && isFinite(options.limit) && !options.returnPointerIds)) { + builder.limit(options.offset ?? 0, options.limit) + } + // sort + if (options.sortBy) { + builder.order(Order.by(this.formatProperty(options.sortBy))[options.sortDesc ? "desc" : "asc"]) + } + if (where) builder.where(where); + } + + /** + * replace all .s with __s, except the last one + */ + private formatProperty(prop: string) { + // + return prop.replace(/\.(?=.*[.].*)/g, '__') + } + + /** + * replace last __ with . + */ + private getTableProperty(prop: string) { + return prop.replace(/__(?!.*__.*)/, '.') + } + + private buildQueryConditions(builder: Query, match: unknown, joins: Map, collectedTableTypes:Set, collectedIdentifiers:Set, valueType:Type, namespacedKey?: string, previousKey?: string, computedProperties?: Record>): Where|undefined { const matchOrs = match instanceof Array ? match : [match] - const entryIdentifier = previousKey ? previousKey + '.' + namespacedKey : namespacedKey // address.street + let entryIdentifier = previousKey ? previousKey + '.' + namespacedKey : namespacedKey // address.street + const underscoreIdentifier = entryIdentifier?.replaceAll(".", "__") + let where: Where|undefined; + let insertedConditionForIdentifier = true; // only set to false if recursing let isPrimitiveArray = true; for (const or of matchOrs) { @@ -719,11 +808,18 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } + const rememberEntryIdentifier = computedProperties && entryIdentifier && !(entryIdentifier in computedProperties); + + // rename entry identifier + if (rememberEntryIdentifier && entryIdentifier) { + entryIdentifier = underscoreIdentifier + } + // only primitive array, use IN selector if (isPrimitiveArray) { if (!namespacedKey) throw new Error("missing namespacedKey"); - if (matchOrs.length == 1) return Where.eq(entryIdentifier!, matchOrs[0]) - else return Where.in(entryIdentifier!, matchOrs) + if (matchOrs.length == 1) where = Where.eq(entryIdentifier!, matchOrs[0]) + else where = Where.in(entryIdentifier!, matchOrs) } else { @@ -764,6 +860,50 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const condition = or as MatchCondition wheresOr.push(Where.ne(entryIdentifier!, condition.data)) } + else if (or.type == MatchConditionType.CONTAINS) { + insertedConditionForIdentifier = false; + const condition = or as MatchCondition + const propertyType = valueType.template[namespacedKey]; + const tableAName = this.#typeToTableName(valueType) + '.' + namespacedKey // User.address + + if (propertyType.base_type == Type.std.Set) { + joins.set( + namespacedKey, + Join + .left(`${this.#metaTables.sets.name}`, namespacedKey) + .on(`${namespacedKey}.${this.#pointerMysqlColumnName}`, tableAName)) + ; + const values = [...condition.data]; + // group values by type + const valuesByType = Map.groupBy(values, v => v instanceof Date ? "time" : typeof v); + for (const [type, vals] of valuesByType) { + const columnName = { + string: "value_text", + number: "value_decimal", + bigint: "value_integer", + boolean: "value_boolean", + function: "value_dxb", + time: "value_time", + object: "value_dxb", + symbol: "value_dxb", + undefined: "value_dxb", + }[type]; + + if (columnName) { + const identifier = rememberEntryIdentifier ? `${namespacedKey}__${columnName}` : `${namespacedKey}.${columnName}` + if (rememberEntryIdentifier) collectedIdentifiers.add(identifier) + + if (vals.length == 1) wheresOr.push(Where.eq(identifier, vals[0])) + else wheresOr.push(Where.in(identifier, vals)) + } + else { + throw new Error("Unsupported type for MatchConditionType.CONTAINS: " + type); + } + } + + } + else throw new Error("Unsupported type for MatchConditionType.CONTAINS: " + or.type); + } else { throw new Error("Unsupported match condition type " + or.type) } @@ -776,39 +916,62 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (ptr instanceof Pointer) { if (!namespacedKey) throw new Error("missing namespacedKey"); wheresOr.push(Where.eq(entryIdentifier!, ptr.id)) - continue; } - // only enter after first recursion - if (namespacedKey) { - collectedTableTypes.add(valueType); - collectedTableTypes.add(valueType.template[namespacedKey]); + else { + insertedConditionForIdentifier = false; - const tableAName = this.#typeToTableName(valueType) + '.' + namespacedKey // User.address - const tableBName = this.#typeToTableName(valueType.template[namespacedKey]); // Address - const tableBIdentifier = namespacedKey + '.' + this.#pointerMysqlColumnName - // Join Adddreess on address._ptr_id = User.address - joins.set(namespacedKey, Join.left(`${tableBName}`, namespacedKey).on(tableBIdentifier, tableAName)); - valueType = valueType.template[namespacedKey]; - } + // only enter after first recursion + if (namespacedKey) { - const whereAnds:Where[] = [] - for (const [key, value] of Object.entries(or)) { - // const nestedKey = namespacedKey ? namespacedKey + "." + key : key; - const condition = this.buildQueryConditions(builder, value, joins, collectedTableTypes, valueType, key, namespacedKey); - if (condition) whereAnds.push(condition) - } - if (whereAnds.length > 1) wheresOr.push(Where.and(...whereAnds)) - else if (whereAnds.length) wheresOr.push(whereAnds[0]) + const propertyType = valueType.template[namespacedKey]; + if (!propertyType) throw new Error("Property '" + namespacedKey + "' does not exist in type " + valueType); + if (propertyType.is_primitive) throw new Error("Tried to match primitive type " + propertyType + " against an object (" + entryIdentifier?.replaceAll("__",".")??namespacedKey + ")") + + collectedTableTypes.add(valueType); + collectedTableTypes.add(propertyType); + + const tableAName = rememberEntryIdentifier ? this.getTableProperty(entryIdentifier!) : entryIdentifier!// this.#typeToTableName(valueType) + '.' + namespacedKey // User.address + const tableBName = this.#typeToTableName(propertyType); // Address + const tableBIdentifier = underscoreIdentifier + '.' + this.#pointerMysqlColumnName + // Join Adddreess on address._ptr_id = User.address + joins.set( + underscoreIdentifier!, + Join + .left(`${tableBName}`, underscoreIdentifier) + .on(tableBIdentifier, tableAName) + ); + valueType = valueType.template[namespacedKey]; + } + + const whereAnds:Where[] = [] + for (const [key, value] of Object.entries(or)) { + + // make sure the key exists in the type + if (!valueType.template[key] && !(computedProperties && key in computedProperties)) throw new Error("Property '" + key + "' does not exist in type " + valueType); + + const condition = this.buildQueryConditions(builder, value, joins, collectedTableTypes, collectedIdentifiers, valueType, key, underscoreIdentifier, computedProperties); + if (condition) whereAnds.push(condition) + } + if (whereAnds.length > 1) wheresOr.push(Where.and(...whereAnds)) + else if (whereAnds.length) wheresOr.push(whereAnds[0]) + } } else { if (!namespacedKey) throw new Error("missing namespacedKey"); wheresOr.push(Where.eq(entryIdentifier!, or)) } } - if (wheresOr.length) return Where.or(...wheresOr) + if (wheresOr.length) where = Where.or(...wheresOr) + } + + + if (rememberEntryIdentifier && insertedConditionForIdentifier && entryIdentifier) { + collectedIdentifiers.add(entryIdentifier); } + return where; + } @@ -967,22 +1130,24 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // is set pointer else if (table == this.#metaTables.sets.name) { - const values = await this.#query<{value_text:string, value_integer:number, value_boolean:boolean, value_time:Date, value_pointer:string, value_dxb:string}>( + const values = await this.#query<{value_text:string, value_integer:number, value_decimal:number, value_boolean:boolean, value_time:Date, value_pointer:string, value_dxb:string}>( new Query() .table(this.#metaTables.sets.name) - .select("value_text", "value_integer", "value_boolean", "value_time", "value_pointer", "value_dxb") + .select("value_text", "value_integer", "value_decimal", "value_boolean", "value_time", "value_pointer", "value_dxb") .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) .where(Where.ne("hash", "")) .build() ) + const result = new Set() - for (const {value_text, value_integer, value_boolean, value_time, value_pointer, value_dxb} of values) { - if (value_text !== undefined) result.add(value_text) - else if (value_integer !== undefined) result.add(value_integer) - else if (value_boolean !== undefined) result.add(value_boolean) - else if (value_time !== undefined) result.add(value_time) - else if (value_pointer !== undefined) result.add(await Pointer.load(value_pointer)) - else if (value_dxb !== undefined) result.add(await Runtime.decodeValue(this.#stringToBinary(value_dxb))) + for (const {value_text, value_integer, value_decimal, value_boolean, value_time, value_pointer, value_dxb} of values) { + if (value_text != undefined) result.add(value_text) + else if (value_integer != undefined) result.add(BigInt(value_integer)) + else if (value_decimal != undefined) result.add(value_decimal) + else if (value_boolean != undefined) result.add(value_boolean) + else if (value_time != undefined) result.add(value_time) + else if (value_pointer != undefined) result.add(await Pointer.load(value_pointer)) + else if (value_dxb != undefined) result.add(await Runtime.decodeValue(this.#stringToBinary(value_dxb))) } return result; } diff --git a/storage/storage.ts b/storage/storage.ts index 14801541..25dd37d5 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -36,10 +36,6 @@ export const site_suffix = (()=>{ })(); -export const comparatorKeys = [ - "=", "!=", ">", ">=", "<", "<=" -] as const - type AtomicMatchInput = T | ( T extends string ? @@ -52,14 +48,13 @@ type _MatchInput = ( T extends object ? { - [K in keyof T]?: MatchInputValue + [K in Exclude]?: MatchInputValue } : AtomicMatchInput|AtomicMatchInput[] ) type MatchInputValue = _MatchInput| // exact match - _MatchInput[]| // or match - Partial> // comparison matches + _MatchInput[] // or match export type MatchInput = MatchInputValue @@ -97,6 +92,10 @@ export type MatchOptions = { * Return pointer ids of matched items */ returnPointerIds?: boolean, + /** + * Custom computed properties for match query + */ + computedProperties?: Record> } export type MatchResult = Options["returnAdvanced"] extends true ? @@ -121,18 +120,21 @@ export enum MatchConditionType { GREATER_THAN = "GREATER_THAN", LESS_OR_EQUAL = "LESS_OR_EQUAL", GREATER_OR_EQUAL = "GREATER_OR_EQUAL", - NOT_EQUAL = "NOT_EQUAL" + NOT_EQUAL = "NOT_EQUAL", + CONTAINS = "CONTAINS" } export type MatchConditionData = T extends MatchConditionType.BETWEEN ? [V, V] : T extends MatchConditionType.LESS_THAN|MatchConditionType.GREATER_THAN|MatchConditionType.LESS_OR_EQUAL|MatchConditionType.GREATER_OR_EQUAL|MatchConditionType.NOT_EQUAL ? V : + T extends MatchConditionType.CONTAINS ? + V : never export class MatchCondition { - constructor( + private constructor( public type: Type, public data: MatchConditionData ) {} @@ -148,6 +150,31 @@ export class MatchCondition { static greaterThan(value: V) { return new MatchCondition(MatchConditionType.GREATER_THAN, value) } + + static contains(...values: V[]) { + return new MatchCondition(MatchConditionType.CONTAINS, new Set(values)) + } +} + +export enum MatchComputedPropertyType { + GEOGRAPHIC_DISTANCE = "GEOGRAPHIC_DISTANCE" +} + +export type MatchComputedPropertyData = + Type extends MatchComputedPropertyType.GEOGRAPHIC_DISTANCE ? + {pointA: {lat: number|string, lon: number|string}, pointB: {lat: number|string, lon: number|string}} : + never + +export class MatchComputedProperty { + + private constructor( + public type: Type, + public data: MatchComputedPropertyData + ) {} + + static geographicDistance(pointA: {lat: number|string, lon: number|string}, pointB: {lat: number|string, lon: number|string}) { + return new MatchComputedProperty(MatchComputedPropertyType.GEOGRAPHIC_DISTANCE, {pointA, pointB}) + } } diff --git a/utils/match.ts b/utils/match.ts index 0bc33617..704a97d7 100644 --- a/utils/match.ts +++ b/utils/match.ts @@ -49,15 +49,8 @@ function _match(value: unknown, match: unknown) { for (const matchEntry of matchOrs) { let isMatch = true; - // is comparator object - if (typeof matchEntry === "object" && Object.keys(matchEntry).some(key => comparatorKeys.includes(key as any))) { - if (!compare(value, matchEntry)) { - isMatch = false; - break; - } - } // nested object - else if (value && typeof value == "object") { + if (value && typeof value == "object") { // identical object if (value === matchEntry) isMatch = true; // nested match @@ -81,81 +74,4 @@ function _match(value: unknown, match: unknown) { // no match found return false; -} - - -function compare(value: unknown, comparatorObj: Partial>) { - let isMatch = true; - for (const [comparator, val] of Object.entries(comparatorObj)) { - // special comparison keys - if (comparator === "=") { - if (value != val) { - isMatch = false; - break; - } - else continue; - } - else if (comparator === "!=") { - if (value == val) { - isMatch = false; - break; - } - else continue; - } - else if (comparator === ">") { - if ((value as any) <= (val as any)) { - isMatch = false; - break; - } - else continue; - } - else if (comparator === ">=") { - if ((value as any) < (val as any)) { - isMatch = false; - break; - } - else continue; - } - else if (comparator === "<") { - if ((value as any) >= (val as any)) { - isMatch = false; - break; - } - else continue; - } - else if (comparator === "<=") { - if ((value as any) > (val as any)) { - isMatch = false; - break; - } - else continue; - } - } - - return isMatch; -} - - - -// match(users, [{ -// name: "John", -// age: [1,2,3], -// address: { -// email: "x@t" -// } -// }, {name: "yxyx"}]) - -/* -SELECT _ptr_id - -FROM __datex_items -JOIN Person ON __datex_items._ptr_id = Person._ptr_id -JOIN Occupation ON Occupation._ptr_id = Person.occupation - -WHERE `key` LIKE "dxset::$D505B7E7C20Ex4E0749C88DE8EB%" -AND `first_name` LIKE "S%" -AND Occupation.degree = "Diplom (FH)" -AND Occupation.degree = "Diplom (FH)" -AND Occupation.degree = "Diplom (FH)" -AND Occupation.degree = "Diplom (FH)" -*/ \ No newline at end of file +} \ No newline at end of file From 081a3f124c1b15d3c482cc2bbcfb661b00a44802 Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 24 Feb 2024 21:48:23 +0100 Subject: [PATCH 20/56] pointer property gc fixes (unrelated) --- runtime/pointers.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/runtime/pointers.ts b/runtime/pointers.ts index a583463a..c6112bff 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -574,7 +574,7 @@ export class PointerProperty extends Ref { #leak_js_properties: boolean - public pointer?: Pointer; + public readonly pointer?: Pointer; private lazy_pointer?: LazyPointer; private constructor(pointer: Pointer|LazyPointer|undefined, public key: any, leak_js_properties = false) { @@ -596,8 +596,21 @@ export class PointerProperty extends Ref { private setPointer(ptr: Pointer) { this.pointer = ptr; this.pointer.is_persistent = true; // TODO: make unpersistent when pointer property deleted + if (!PointerProperty.synced_pairs.has(ptr)) PointerProperty.synced_pairs.set(ptr, new Map()); - PointerProperty.synced_pairs.get(ptr)!.set(this.key, this); // save in map + PointerProperty.synced_pairs.get(ptr)!.set(this.key, new WeakRef(this)); // save in map + PointerProperty.registerForFinalization(this); + } + + private static finalizationRegistry = new FinalizationRegistry((ptr:Pointer) => { + this.synced_pairs.delete(ptr) + ptr.is_persistent = false; + console.log("freed pointer bound to pointer property: " + ptr) + }); + + private static registerForFinalization(ref:PointerProperty) { + if (!ref.pointer) throw new Error("Cannot register pointer property for finalization, no pointer"); + this.finalizationRegistry.register(ref, ref.pointer!); } /** @@ -610,7 +623,7 @@ export class PointerProperty extends Ref { else callback(this, this); } - private static synced_pairs = new WeakMap>() + private static synced_pairs = new WeakMap>>() // TODO: use InferredPointerProperty (does not collapse) /** @@ -627,7 +640,14 @@ export class PointerProperty extends Ref { if (pointer instanceof Pointer) { if (!this.synced_pairs.has(pointer)) this.synced_pairs.set(pointer, new Map()); - if (this.synced_pairs.get(pointer)!.has(key)) return this.synced_pairs.get(pointer)!.get(key)!; + if (this.synced_pairs.get(pointer)!.has(key)) { + const weakRef = this.synced_pairs.get(pointer)!.get(key); + const pointerProperty = weakRef.deref(); + if (pointerProperty) return pointerProperty; + else { + this.synced_pairs.get(pointer)!.delete(key); + } + } } return new PointerProperty(pointer, key, leak_js_properties); From 4ad6376d37eb65c674b14adcda3cd38b23849d80 Mon Sep 17 00:00:00 2001 From: benStre Date: Sun, 25 Feb 2024 00:32:39 +0100 Subject: [PATCH 21/56] fix memory leaks and related issues (unrelated) --- runtime/pointers.ts | 23 +++++++-------- runtime/runtime.ts | 16 ++++++++-- storage/storage-locations/sql-db.ts | 46 ++++++++++++++++++++++------- storage/storage.ts | 15 +++++++--- types/function-utils.ts | 2 +- types/js-function.ts | 4 +-- utils/message_logger.ts | 8 ++++- utils/weak-action.ts | 31 +++++++++++++++---- 8 files changed, 105 insertions(+), 40 deletions(-) diff --git a/runtime/pointers.ts b/runtime/pointers.ts index c6112bff..76c6c9f1 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -574,6 +574,8 @@ export class PointerProperty extends Ref { #leak_js_properties: boolean + private _strongRef?: any // strong reference to own pointer to prevent garbage collection + public readonly pointer?: Pointer; private lazy_pointer?: LazyPointer; @@ -594,24 +596,15 @@ export class PointerProperty extends Ref { } private setPointer(ptr: Pointer) { + // @ts-ignore private this.pointer = ptr; - this.pointer.is_persistent = true; // TODO: make unpersistent when pointer property deleted + + this._strongRef = ptr.val; if (!PointerProperty.synced_pairs.has(ptr)) PointerProperty.synced_pairs.set(ptr, new Map()); PointerProperty.synced_pairs.get(ptr)!.set(this.key, new WeakRef(this)); // save in map - PointerProperty.registerForFinalization(this); } - private static finalizationRegistry = new FinalizationRegistry((ptr:Pointer) => { - this.synced_pairs.delete(ptr) - ptr.is_persistent = false; - console.log("freed pointer bound to pointer property: " + ptr) - }); - - private static registerForFinalization(ref:PointerProperty) { - if (!ref.pointer) throw new Error("Cannot register pointer property for finalization, no pointer"); - this.finalizationRegistry.register(ref, ref.pointer!); - } /** * Called when the bound lazy pointer is loaded. @@ -3418,6 +3411,8 @@ export class Pointer extends Ref { if (!(await endpoint.isOnline())) { this.clearEndpointSubscriptions(endpoint); this.clearEndpointPermissions(endpoint) + // TODO: this should ideally directly be handleded by the runtime + Runtime.clearEndpointScopes(endpoint); } } } @@ -4418,7 +4413,9 @@ export class Pointer extends Ref { // key specific observers if (key!=undefined) { for (const [o, options] of this.change_observers.get(key)||[]) { - if ((!options?.types || options.types.includes(type)) && !(is_transform && options?.ignore_transforms) && (!is_child_update || !options || options.recursive)) promises.push(o(value, key, type, is_transform, is_child_update, previous, atomic_id)); + if ((!options?.types || options.types.includes(type)) && !(is_transform && options?.ignore_transforms) && (!is_child_update || !options || options.recursive)) { + promises.push(o(value, key, type, is_transform, is_child_update, previous, atomic_id)); + } } // bound observers for (const [object, entries] of this.bound_change_observers.entries()) { diff --git a/runtime/runtime.ts b/runtime/runtime.ts index 58e3d2f6..3fbe736a 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -1274,6 +1274,15 @@ export class Runtime { else return [] } + /** + * Removes all active datex scopes for an endpoint + */ + public static clearEndpointScopes(endpoint: Endpoint) { + const removeCount = this.active_datex_scopes.get(endpoint)?.size; + this.active_datex_scopes.delete(endpoint); + if (removeCount) logger.debug("removed " + removeCount + " datex scopes for " + endpoint); + } + /** * Creates default static scopes * + other async initializations @@ -1828,6 +1837,7 @@ export class Runtime { header.sender.setOnline(false) Pointer.clearEndpointSubscriptions(header.sender) Pointer.clearEndpointPermissions(header.sender) + this.clearEndpointScopes(header.sender); } else { logger.error("ignoring unsigned GOODBYE message") @@ -2027,7 +2037,7 @@ export class Runtime { // save persistent memory if (scope.persistent_vars) { const identifier = scope.context_location.toString() - for (let name of scope.persistent_vars) Runtime.saveScopeMemoryValue(identifier, name, scope.internal_vars[name]); + for (const name of scope.persistent_vars) Runtime.saveScopeMemoryValue(identifier, name, scope.internal_vars[name]); } // cleanup @@ -7384,8 +7394,8 @@ Type.get("js:Function").setJSInterface({ }, apply_value(parent, args = []) { - if (args instanceof Tuple) return parent.call(...args.toArray()) - else return parent.call(args) + if (args instanceof Tuple) return parent.handleCall(...args.toArray()) + else return parent.handleCall(args) }, }); diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index ca9d9f35..b2a3bd81 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -109,6 +109,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { this.#initializing = true; await this.#connect(); await this.#setupMetaTables(); + await MessageLogger.init(); // required for decompiling this.#initializing = false; this.#initialized = true; } @@ -375,10 +376,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { * makes sure all DATEX meta tables exist in the database */ async #setupMetaTables() { - for (const definition of Object.values(this.#metaTables)) { - const createdNew = await this.#createTableIfNotExists(definition); - if (createdNew) this.log?.("Created meta table '" + definition.name + "'") - } + await Promise.all( + Object + .values(this.#metaTables) + .map(definition => this.#createTableIfNotExists(definition)) + ) } async #getTableColumns(tableName: string) { @@ -513,16 +515,14 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { object[colName] = `\u0001${foreignPointerPlaceholders.length}` foreignPointerPlaceholders.push("$"+ptrId) } - else { - logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) - } + // otherwise, property is null/undefined } // is blob, assume it is a DXB value else if (type == "blob") { object[colName] = `\u0001${foreignPointerPlaceholders.length}` try { // TODO: fix decompiling - foreignPointerPlaceholders.push(MessageLogger.decompile(object[colName] as ArrayBuffer, false, false, false)||"'error: empty'") + foreignPointerPlaceholders.push(MessageLogger.decompile(object[colName] as ArrayBuffer, false, false, false)||"void") } catch (e) { console.error("error decompiling", object[colName], e) @@ -1182,9 +1182,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (typeof object[colName] == "string") { object[colName] = await Pointer.load(object[colName] as string); } - else { - logger.error("Cannot get pointer value for property " + colName + " in object " + pointerId + " - " + table) - } + // else property is null/undefined } // is blob, assume it is a DXB value else if (type == "blob") { @@ -1224,6 +1222,32 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return value ? this.#stringToBinary(value) : null; } + // is set pointer + else if (table == this.#metaTables.sets.name) { + const values = await this.#query<{value_text:string, value_integer:number, value_decimal:number, value_boolean:boolean, value_time:Date, value_pointer:string, value_dxb:string}>( + new Query() + .table(this.#metaTables.sets.name) + .select("value_text", "value_integer", "value_decimal", "value_boolean", "value_time", "value_pointer", "value_dxb") + .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + .where(Where.ne("hash", "")) + .build() + ) + let setString = ` [` + const setEntries:string[] = [] + + for (const {value_text, value_integer, value_decimal, value_boolean, value_time, value_pointer, value_dxb} of values) { + if (value_text != undefined) setEntries.push(Runtime.valueToDatexStringExperimental(value_text)) + else if (value_integer != undefined) setEntries.push(Runtime.valueToDatexStringExperimental(value_integer)) + else if (value_decimal != undefined) setEntries.push(Runtime.valueToDatexStringExperimental(value_decimal)) + else if (value_boolean != undefined) setEntries.push(Runtime.valueToDatexStringExperimental(value_boolean)) + else if (value_time != undefined) setEntries.push(Runtime.valueToDatexStringExperimental(value_time)) + else if (value_pointer != undefined) setEntries.push(Runtime.valueToDatexStringExperimental(await Pointer.load(value_pointer))) + else if (value_dxb != undefined) setEntries.push(MessageLogger.decompile(this.#stringToBinary(value_dxb), false, false, false)) + } + setString += setEntries.join(",") + "]" + return Compiler.compile(setString, [], {sign: false, encrypt: false, to: Datex.Runtime.endpoint, preemptive_pointer_init: false}, false) as ArrayBuffer; + } + // is templated pointer else { return this.#getTemplatedPointerValueDXB(pointerId, table); diff --git a/storage/storage.ts b/storage/storage.ts index 25dd37d5..b829b2af 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -327,7 +327,7 @@ export class Storage { static item_deps_prefix = "deps::dxitem::" static subscriber_cache_prefix = "subscribers::" - static #storage_active_pointers = new Set(); + static #storage_active_pointers = new IterableWeakSet(); static #storage_active_pointer_ids = new Set(); /** @@ -477,14 +477,14 @@ export class Storage { } // update pointers - for (const ptr of this.#storage_active_pointers) { + for (const ptr of [...this.#storage_active_pointers]) { try { c++; const res = this.setPointer(ptr, true, location); if (res instanceof Promise) res.catch(()=>{}) } catch (e) {} } - for (const id of this.#storage_active_pointer_ids) { + for (const id of [...this.#storage_active_pointer_ids]) { try { c++; const ptr = Pointer.get(id); @@ -744,7 +744,7 @@ export class Storage { } - private static synced_pointers = new Set(); + private static synced_pointers = new WeakSet(); static syncPointer(pointer: Pointer, location: StorageLocation|undefined = this.#primary_location) { if (!this.#auto_sync_enabled) return; @@ -1328,6 +1328,13 @@ export class Storage { } } + // remove internal localstorage entries + for (const key of Object.keys(localStorage)) { + if (key.startsWith(this.rc_prefix) || key.startsWith(this.item_deps_prefix) || key.startsWith(this.pointer_deps_prefix) || key.startsWith(this.meta_prefix)) { + localStorage.removeItem(key); + } + } + } /** diff --git a/types/function-utils.ts b/types/function-utils.ts index c3ccd20b..08491168 100644 --- a/types/function-utils.ts +++ b/types/function-utils.ts @@ -60,7 +60,7 @@ declare global { function getUsedVars(fn: (...args:unknown[])=>unknown) { const source = fn.toString(); - const usedVarsSource = source.match(/^(?:(?:[\w\s*])+\(.*\)\s*{|\(.*\)\s*=>\s*{?|.*\s*=>\s*{?)\s*use\s*\(([\s\S]*?)\)/)?.[1] + const usedVarsSource = source.match(/^(?:(?:[\w\s*])+\(.*?\)\s*{|\(.*?\)\s*=>\s*{?|.*?\s*=>\s*{?)\s*use\s*\(([\s\S]*?)\)/)?.[1] if (!usedVarsSource) return {}; const usedVars = usedVarsSource.split(",").map(v=>v.trim()).filter(v=>!!v) diff --git a/types/js-function.ts b/types/js-function.ts index f9ced748..ee7a1cfc 100644 --- a/types/js-function.ts +++ b/types/js-function.ts @@ -49,14 +49,14 @@ export class JSTransferableFunction extends ExtensibleFunction { } - call(...args:any[]) { + handleCall(...args:any[]) { return this.#fn(...args) } // waits until all lazy dependencies are resolved and then calls the function async callLazy() { await this.lazyResolved; - return this.call() + return this.handleCall() } public get hasUnresolvedLazyDependencies() { diff --git a/utils/message_logger.ts b/utils/message_logger.ts index 9a057265..486a9873 100644 --- a/utils/message_logger.ts +++ b/utils/message_logger.ts @@ -31,7 +31,13 @@ export class MessageLogger { static #initialized = false; static async init() { - await wasm_init() + if (this.#initialized) return; + try { + await wasm_init() + } + catch (e) { + console.error(e) + } this.#initialized = true; } diff --git a/utils/weak-action.ts b/utils/weak-action.ts index e7de9fd7..395ce3d0 100644 --- a/utils/weak-action.ts +++ b/utils/weak-action.ts @@ -5,12 +5,13 @@ import { isolatedScope } from "./isolated-scope.ts"; * If one of the weak dependencies is garbage collected, an optional deinit function is called. * @param weakRefs * @param action an isolated callback function that provides weak references. External dependency variable must be explicitly added with use() - * @param deinit an isolated callback function that is callled on garbage collection. External dependency variable must be explicitly added with use() + * @param deinit an isolated callback function that is called on garbage collection. External dependency variable must be explicitly added with use() */ -export function weakAction, R>(weakDependencies: T, action: (values: {[K in keyof T]: WeakRef}) => R, deinit?: (actionResult: R, collectedVariable: keyof T) => unknown) { +export function weakAction, R, D extends Record|undefined>(weakDependencies: T, action: (values: {[K in keyof T]: WeakRef}) => R, deinit?: (actionResult: R, collectedVariable: keyof T, weakDeinitDependencies: D) => unknown, weakDeinitDependencies?: D) { const weakRefs = _getWeakRefs(weakDependencies); + const weakDeinitRefs = weakDeinitDependencies ? _getWeakRefs(weakDeinitDependencies) : undefined; - let result:R; + let result:R|WeakRef; action = isolatedScope(action); @@ -22,7 +23,26 @@ export function weakAction, R>(weakDependencie const deinitHandler = (k: string) => { registries.delete(registry) - deinitFn(result, k); + + // unwrap all deinit weak refs + const weakDeinitDeps = weakDeinitRefs && Object.fromEntries( + Object.entries(weakDeinitRefs).map(([k, v]) => [k, v.deref()]) + ) + // check if all deinit weak refs are still alive, otherwise return + if (weakDeinitDeps) { + for (const v of Object.values(weakDeinitDeps)) { + if (v === undefined) { + return; + } + } + } + + const unwrappedResult = result instanceof WeakRef ? result.deref() : result; + if (result instanceof WeakRef && unwrappedResult === undefined) { + return; + } + + deinitFn(unwrappedResult!, k, weakDeinitDeps as D); } const registry = new FinalizationRegistry(deinitHandler); registries.add(registry) @@ -39,7 +59,8 @@ export function weakAction, R>(weakDependencie } // call action once - result = action(weakRefs); + const actionResult = action(weakRefs); + result = (actionResult && (typeof actionResult === "object" || typeof actionResult == "function")) ? new WeakRef(actionResult) : actionResult; } function _getWeakRefs>(weakDependencies: T) { From d50a82af0bdb472bc9472f9338ce8450a97072a0 Mon Sep 17 00:00:00 2001 From: benStre Date: Sun, 25 Feb 2024 14:25:08 +0100 Subject: [PATCH 22/56] change @property decorator parameter to type instead of name brreaking change) --- docs/manual/15 Storage Collections.md | 39 ++++++++++++++++++++++----- js_adapter/js_class_adapter.ts | 10 +++++-- js_adapter/legacy_decorators.ts | 7 ++--- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/docs/manual/15 Storage Collections.md b/docs/manual/15 Storage Collections.md index d23a6305..4e2522bc 100644 --- a/docs/manual/15 Storage Collections.md +++ b/docs/manual/15 Storage Collections.md @@ -1,14 +1,17 @@ # Storage Collections -DATEX allows the usage of native Set and Map objects. Those types are stored in RAM and may impact performance when it's data is getting to large. -For this reason DATEX provides special storage collections that allow the handling of massive amounts of data more efficiently. -The API is similar to the native JavaScript collections with the major difference that their instance methods are asynchronous. -To get the size of the collection it is recommended to use the asynchronous `getSize` method. +The native `Set` and `Map` objects can be used with DATEX cross-network and as persistent values, +but because their entries are only stored in RAM, they are not ideal for large amounts of data. + +For this reason, DATEX provides special collection types (`StorageMap`/`StorageSet`) that handle large amounts of data more efficiently by outsourcing entries to a pointer storage location instead of keeping everything in RAM. + +The API is similar to `Set`/`Map` with the major difference that the instance methods are asynchronous. > [!NOTE] -> The storage collections are not stored persistently by default. To store persistent data refer to [Eternal Pointers](./05%20Eternal%20Pointers.md). +> Storage collections are not stored persistently by default as their name might imply. To store storage collections persistently, use [Eternal Pointers](./05%20Eternal%20Pointers.md). + +## StorageSets -## StorageSet ```ts import "datex-core-legacy/types/storage-set.ts"; const mySet = new StorageSet(); @@ -22,7 +25,8 @@ await mySet.getSize(); // Returns the size of the StorageSet (1) await mySet.clear(); // Clear StorageSet ``` -## StorageMap +## StorageMaps + ```ts import "datex-core-legacy/types/storage-map.ts"; const myMap = new StorageMap(); @@ -35,3 +39,24 @@ for await (const [key, value] of myMap) { // Iterate over entries await mySet.getSize(); // Returns the size of the StorageMap (1) await myMap.clear(); // Clear StorageMap ``` + + +## Pattern Matching + +Entries of a `StorageSet` can be efficiently queried by using the builtin pattern matcher. +For supported storage locations, the pattern matching is directly performed in storage and non-matching entries are never loaded into RAM. + +### Selecting by property + +The easiest way to match entries in a storage set is to provide a required property: + +```ts +@sync class User { + @property(string) declare name: string + @property(number) declare age: number +} + +const users = new StorageSet(); +// get all users with age == 18 +const usersAge18 = await users.match(User, {age: 18}); +``` \ No newline at end of file diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index ae5ae65f..e4a766e7 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -320,8 +320,14 @@ export class Decorators { if (kind != "field" && kind != "getter" && kind != "setter" && kind != "method") logger.error("Invalid use of @property decorator"); else { - if (is_static) setMetadata(Decorators.STATIC_PROPERTY, params?.[0] ?? name) - else setMetadata(Decorators.PROPERTY, params?.[0] ?? name) + if (is_static) setMetadata(Decorators.STATIC_PROPERTY, name) + else setMetadata(Decorators.PROPERTY, name) + + // type + if (params?.[0]) { + const type = normalizeType(params[0]); + setMetadata(Decorators.FORCE_TYPE, type) + } } } diff --git a/js_adapter/legacy_decorators.ts b/js_adapter/legacy_decorators.ts index cc75aafa..226b1a50 100644 --- a/js_adapter/legacy_decorators.ts +++ b/js_adapter/legacy_decorators.ts @@ -247,10 +247,11 @@ export function template(...args:any[]): any { return handleDecoratorArgs(args, Decorators.template); } -export function property(name:string|number):any -export function property(target: any, name?: string, method?:any):any +// export function property(name:string|number):any +export function property(type:string|Type|Class):any +export function property(target: any, name: string, method?:any):any export function property(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.property); + return handleDecoratorArgs(args, Decorators.property, args[0] && typeof args[0] == "function"); } export function jsdoc(target: any, name?: string, method?:any):any From 7d85546e4e4e5e311a0616398aa941b83851f433 Mon Sep 17 00:00:00 2001 From: benStre Date: Mon, 26 Feb 2024 16:53:14 +0100 Subject: [PATCH 23/56] extend pattern matching docs --- docs/manual/15 Storage Collections.md | 201 +++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 6 deletions(-) diff --git a/docs/manual/15 Storage Collections.md b/docs/manual/15 Storage Collections.md index 4e2522bc..24b2c7dd 100644 --- a/docs/manual/15 Storage Collections.md +++ b/docs/manual/15 Storage Collections.md @@ -13,7 +13,8 @@ The API is similar to `Set`/`Map` with the major difference that the instance me ## StorageSets ```ts -import "datex-core-legacy/types/storage-set.ts"; +import { StorageSet } from "datex-core-legacy/types/storage-set.ts"; + const mySet = new StorageSet(); await mySet.add(123); // Add 123 to the StorageSet @@ -28,7 +29,8 @@ await mySet.clear(); // Clear StorageSet ## StorageMaps ```ts -import "datex-core-legacy/types/storage-map.ts"; +import { StorageMap } from "datex-core-legacy/types/storage-map.ts"; + const myMap = new StorageMap(); await myMap.set("myKey", 123); // Add key 'myKey' with value 123 to the StorageMap @@ -44,19 +46,206 @@ await myMap.clear(); // Clear StorageMap ## Pattern Matching Entries of a `StorageSet` can be efficiently queried by using the builtin pattern matcher. -For supported storage locations, the pattern matching is directly performed in storage and non-matching entries are never loaded into RAM. +For supported storage locations (e.g. sql storage), the pattern matching is directly performed in storage and non-matching entries are never loaded into RAM. + +> [!NOTE] +> Pattern matching currently only works with @sync class objects. ### Selecting by property -The easiest way to match entries in a storage set is to provide a required property: +The easiest way to match entries in a storage set is to provide one or multiple required property values: ```ts +import { StorageSet } from "datex-core-legacy/types/storage-set.ts"; +import { Time } from "unyt_core/datex_all.ts"; + @sync class User { @property(string) declare name: string @property(number) declare age: number + @property(Time) declare created: Time } const users = new StorageSet(); + // get all users with age == 18 -const usersAge18 = await users.match(User, {age: 18}); -``` \ No newline at end of file +const usersAge18 = await users.match( + User, + { + age: 18 + } +); +``` + +### Match Conditions + +Besides exact matches, you can also match properties with certain constraints using match conditions: + +Match between to numbers/dates: +```ts +import { MatchCondition } from "unyt_core/storage/storage.ts"; + +// all users where the "created" timestamp is between now and 7 days ago: +const newUsersLastWeek = users.match( + User, + { + created: MatchCondition.between( + new Time().minus(7, "d"), + new Time() + ) + } +) +``` + +Match not equal: +```ts +// all users which do not have the name "John": +const notJohn = users.match( + User, + { + name: MatchCondition.notEqual("John") + } +) +``` + + +### Return value customization + +#### Limiting + +You can limit the maximum number of returned entries by setting the `limit` option to a number: + +```ts +// get all users with name "Josh", limit to 10 +const joshes = await users.match( + User, + { + name: "Josh" + }, + {limit: 10} +); +``` + +#### Sorting + +You can sort the returned entries by setting the `sortBy` option to a property path: + +```ts +// get all users with age == 18, sorted by their creation timestamp +const usersAge18 = await users.match( + User, + { + age: 18 + }, + {sortBy: 'created'} +); +``` + +Directly sorting values this way in the match query has two significant advantages over sorting +the returned values afterwards, e.g. using `Array.sort`: + * The sorting is normally faster + * When using the `limit` option, sorting is done before applying the `limit`, otherwise only the values remaining within the limit would be sorted + + +#### Returning additional metadata + +When the `returnAdvanced` option is set to `true`, the `match` function returns an object with additional metadata: + +```ts +const {matches, total} = await users.match( + User, + { + name: "Josh" + }, + { + limit: 10, + returnAdvanced: true + } +); + +matches // matching entries: Set +total // total number of matches that would be returned without the limit +``` + + +### Computed properties + +Computed properties provide a way to efficiently match entries in the StorageSet with more complex conditions. +One or multiple computed properties can be specified in the `computedProperties` option. + +#### Geographic Distance + +Calculates the geographic distance of two points provided from literal values or properties: + +Example: +```ts +import { ComputedProperty } from "datex-core-legacy/storage/storage.ts"; + +@sync class Location { + @property(number) declare lat: number + @property(number) declare lon: number +} + +@sync class User { + @property(string) declare name: string + @property(Location) declare location: Location +} + + +const myPosition = {lat: 70.48, lon: -21.96} + +// computed geographic distance between myPosition and a user position +const distance = ComputedProperty.geographicDistance( + // point A (user position) + { + lat: 'location.lat', + lon: 'location.lon' + }, + // point B (my position) + { + lat: myPosition.lat, + lon: myPosition.lon + } +) + +const nearbyJoshes = await users.match( + User, + { + name: "Josh", // name = "Josh" + distance: MatchCondition.lessThan(1000) // distance < 1000m + }, + { + computedProperties: { distance } + } +); +``` + +#### Sum + +Calculates the sum of multiple properties or literal values + +Example: +```ts +@sync class TodoItem { + @property(number) declare completedTaskCount: number + @property(number) declare openTaskCount: number +} +const todoItems = new StorageSet() + + +// sum of completedTaskCount and openTaskCount for a given TodoItem +const totalTaskCount = ComputedProperty.sum( + 'completedTaskCount', + 'openTaskCount' +) + +// match all todo items where the total task count is > 100 +const bigTodoItems = await todoItems.match( + User, + { + totalTaskCount: MatchCondition.greaterThan(100) // totalTaskCount > 100 + }, + { + computedProperties: { totalTaskCount } + } +); +``` From 4cfc39a03c5f81163c3bf93bc611592ce7d83c19 Mon Sep 17 00:00:00 2001 From: benStre Date: Mon, 26 Feb 2024 16:53:29 +0100 Subject: [PATCH 24/56] refactoring --- storage/storage.ts | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/storage/storage.ts b/storage/storage.ts index b829b2af..74c93537 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -95,7 +95,7 @@ export type MatchOptions = { /** * Custom computed properties for match query */ - computedProperties?: Record> + computedProperties?: Record> } export type MatchResult = Options["returnAdvanced"] extends true ? @@ -151,29 +151,48 @@ export class MatchCondition { return new MatchCondition(MatchConditionType.GREATER_THAN, value) } + static lessOrEqual(value: V) { + return new MatchCondition(MatchConditionType.LESS_OR_EQUAL, value) + } + + static greaterOrEqual(value: V) { + return new MatchCondition(MatchConditionType.GREATER_OR_EQUAL, value) + } + + static notEqual(value: V) { + return new MatchCondition(MatchConditionType.NOT_EQUAL, value) + } + static contains(...values: V[]) { return new MatchCondition(MatchConditionType.CONTAINS, new Set(values)) } } -export enum MatchComputedPropertyType { - GEOGRAPHIC_DISTANCE = "GEOGRAPHIC_DISTANCE" +export enum ComputedPropertyType { + GEOGRAPHIC_DISTANCE = "GEOGRAPHIC_DISTANCE", + SUM = "SUM", } -export type MatchComputedPropertyData = - Type extends MatchComputedPropertyType.GEOGRAPHIC_DISTANCE ? +export type ComputedPropertyData = + Type extends ComputedPropertyType.GEOGRAPHIC_DISTANCE ? {pointA: {lat: number|string, lon: number|string}, pointB: {lat: number|string, lon: number|string}} : + Type extends ComputedPropertyType.SUM ? + (number|string)[] : never -export class MatchComputedProperty { +export class ComputedProperty { private constructor( public type: Type, - public data: MatchComputedPropertyData + public data: ComputedPropertyData ) {} static geographicDistance(pointA: {lat: number|string, lon: number|string}, pointB: {lat: number|string, lon: number|string}) { - return new MatchComputedProperty(MatchComputedPropertyType.GEOGRAPHIC_DISTANCE, {pointA, pointB}) + return new ComputedProperty(ComputedPropertyType.GEOGRAPHIC_DISTANCE, {pointA, pointB}) + } + + static sum(...values: (number|string)[]) { + return new ComputedProperty(ComputedPropertyType.SUM, values) } } From f661a4b69966984701d5b40a1aada6f18b9df51d Mon Sep 17 00:00:00 2001 From: benStre Date: Mon, 26 Feb 2024 16:54:18 +0100 Subject: [PATCH 25/56] add computed property sum --- storage/storage-locations/sql-db.ts | 66 +++++++++++++++++++---------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index b2a3bd81..29ba8514 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -18,7 +18,7 @@ import { TypedArray } from "../../utils/global_values.ts"; import { MessageLogger } from "../../utils/message_logger.ts"; import { Join } from "https://deno.land/x/sql_builder@v1.9.2/join.ts"; import { LazyPointer } from "../../runtime/lazy-pointer.ts"; -import { MatchOptions, MatchCondition, MatchConditionType, MatchComputedProperty, MatchComputedPropertyType} from "../storage.ts"; +import { MatchOptions, MatchCondition, MatchConditionType, ComputedProperty, ComputedPropertyType} from "../storage.ts"; import { MatchResult } from "../storage.ts"; import { Time } from "../../types/time.ts"; import { Order } from "https://deno.land/x/sql_builder@v1.9.2/order.ts"; @@ -675,27 +675,15 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { }); for (const [name, value] of Object.entries(options.computedProperties)) { - if (value.type == MatchComputedPropertyType.GEOGRAPHIC_DISTANCE) { - const {pointA, pointB} = value.data; - const mockObject = {} - for (const property of [pointA.lat, pointA.lon, pointB.lat, pointB.lon]) { - // is property, not literal position - if (typeof property == "string") { - let object:Record = mockObject; - let lastParent:Record = mockObject; - let lastProperty: string|undefined - for (const part of property.split(".")) { - if (!object[part]) object[part] = {}; - lastParent = object; - lastProperty = part; - object = object[part]; - } - if (lastParent && lastProperty!=undefined) lastParent[lastProperty] = null; - } - } - // get correct joins - this.buildQueryConditions(builder, mockObject, joins, collectedTableTypes, new Set(), valueType) + if (value.type == ComputedPropertyType.GEOGRAPHIC_DISTANCE) { + const computedProperty = value as ComputedProperty + const {pointA, pointB} = computedProperty.data; + this.addPropertyJoins( + [pointA.lat, pointA.lon, pointB.lat, pointB.lon].filter(v => typeof v == "string") as string[], + builder, joins, valueType, collectedTableTypes + ) + select.push( `ST_Distance_Sphere(point(${ typeof pointA.lon == "string" ? this.formatProperty(pointA.lon) : pointA.lon @@ -708,6 +696,20 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { })) as ${name}` ) } + else if (value.type == ComputedPropertyType.SUM) { + const computedProperty = value as ComputedProperty + this.addPropertyJoins( + computedProperty.data.filter(v => typeof v == "string") as string[], + builder, joins, valueType, collectedTableTypes + ) + select.push(`SUM(${computedProperty.data.map(p => { + if (typeof p == "string") return this.formatProperty(p) + else return p + })}) as ${name}`) + } + else { + throw new Error("Unsupported computed property type " + value.type) + } } builder.select(...select); joins.forEach(join => builder.join(join)); @@ -742,7 +744,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { ptrIds // TODO: atomic operations for multiple queries - const {foundRows} = await this.#queryFirst<{foundRows: number}>("SELECT FOUND_ROWS() as foundRows") ?? {foundRows: -1} + const {foundRows} = (options?.returnAdvanced ? await this.#queryFirst<{foundRows: number}>("SELECT FOUND_ROWS() as foundRows") : null) ?? {foundRows: -1} const result = new Set((await Promise.all(limitedPtrIds.map(ptrId => Pointer.load(ptrId)))).filter(ptr => { if (ptr instanceof LazyPointer) { @@ -764,6 +766,24 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } + private addPropertyJoins(properties: string[], builder: Query, joins: Map, valueType: Type, collectedTableTypes: Set) { + const mockObject = {} + for (const property of properties) { + let object:Record = mockObject; + let lastParent:Record = mockObject; + let lastProperty: string|undefined + for (const part of property.split(".")) { + if (!object[part]) object[part] = {}; + lastParent = object; + lastProperty = part; + object = object[part]; + } + if (lastParent && lastProperty!=undefined) lastParent[lastProperty] = null; + } + // get correct joins + this.buildQueryConditions(builder, mockObject, joins, collectedTableTypes, new Set(), valueType) + } + private appendBuilderConditions(builder: Query, options: MatchOptions, where?: Where) { // limit, do limit later if options.returnPointerIds if (options && (options.limit !== undefined && isFinite(options.limit) && !options.returnPointerIds)) { @@ -791,7 +811,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return prop.replace(/__(?!.*__.*)/, '.') } - private buildQueryConditions(builder: Query, match: unknown, joins: Map, collectedTableTypes:Set, collectedIdentifiers:Set, valueType:Type, namespacedKey?: string, previousKey?: string, computedProperties?: Record>): Where|undefined { + private buildQueryConditions(builder: Query, match: unknown, joins: Map, collectedTableTypes:Set, collectedIdentifiers:Set, valueType:Type, namespacedKey?: string, previousKey?: string, computedProperties?: Record>): Where|undefined { const matchOrs = match instanceof Array ? match : [match] let entryIdentifier = previousKey ? previousKey + '.' + namespacedKey : namespacedKey // address.street From 55fb7261d6b9ea8a284898e0ed7329279ddcc82e Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 27 Feb 2024 12:30:39 +0100 Subject: [PATCH 26/56] update docs --- docs/manual/15 Storage Collections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/manual/15 Storage Collections.md b/docs/manual/15 Storage Collections.md index 24b2c7dd..06dd3d2c 100644 --- a/docs/manual/15 Storage Collections.md +++ b/docs/manual/15 Storage Collections.md @@ -1,7 +1,7 @@ # Storage Collections The native `Set` and `Map` objects can be used with DATEX cross-network and as persistent values, -but because their entries are only stored in RAM, they are not ideal for large amounts of data. +but because their entries are completely stored in RAM, they are not ideal for large amounts of data. For this reason, DATEX provides special collection types (`StorageMap`/`StorageSet`) that handle large amounts of data more efficiently by outsourcing entries to a pointer storage location instead of keeping everything in RAM. From 56a622dda1dee7d5031faac6be35a221162b67d2 Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 27 Feb 2024 18:46:55 +0100 Subject: [PATCH 27/56] refactoring for new decorators --- datex_all.ts | 2 +- datex_short.ts | 17 +- js_adapter/decorators.ts | 64 +++++++ js_adapter/js_class_adapter.ts | 300 +++++++++++++-------------------- mod.ts | 2 +- types/addressing.ts | 1 + types/logic.ts | 4 +- types/struct.ts | 20 ++- utils/global_types.ts | 2 +- 9 files changed, 216 insertions(+), 196 deletions(-) create mode 100644 js_adapter/decorators.ts diff --git a/datex_all.ts b/datex_all.ts index d9906377..bd30ba9c 100644 --- a/datex_all.ts +++ b/datex_all.ts @@ -4,7 +4,7 @@ export * from "./runtime/runtime.ts"; // js_adapter export * from "./js_adapter/js_class_adapter.ts"; -export * from "./js_adapter/legacy_decorators.ts"; +export * from "./js_adapter/decorators.ts"; // utils export type * from "./utils/global_types.ts"; diff --git a/datex_short.ts b/datex_short.ts index 0b782493..c2661cf6 100644 --- a/datex_short.ts +++ b/datex_short.ts @@ -1,10 +1,10 @@ // shortcut functions // import { Datex } from "./datex.ts"; -import { baseURL, Runtime, PrecompiledDXB, Type, Pointer, Ref, PointerProperty, primitive, any_class, Target, IdEndpoint, TransformFunctionInputs, AsyncTransformFunction, TransformFunction, Markdown, 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"; +import { baseURL, Runtime, PrecompiledDXB, Type, Pointer, Ref, PointerProperty, primitive, Target, IdEndpoint, Markdown, 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, dc } from "./datex_all.ts"; /** make decorators global */ -import { assert as _assert, property as _property, sync as _sync, endpoint as _endpoint, template as _template, jsdoc as _jsdoc} from "./datex_all.ts"; +import { assert as _assert, property as _property, struct as _struct, endpoint as _endpoint, sync as _sync} from "./datex_all.ts"; import { effect as _effect, always as _always, reactiveFn as _reactiveFn, asyncAlways as _asyncAlways, 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"; @@ -22,10 +22,13 @@ declare global { const property: typeof _property; const assert: typeof _assert; - const jsdoc: typeof _jsdoc; - const sync: typeof _sync; + const struct: typeof _struct; const endpoint: typeof _endpoint; const always: typeof _always; + /** + * @deprecated Use struct(class {...}) instead; + */ + const sync: typeof _sync; const asyncAlways: typeof _asyncAlways; const reactiveFn: typeof _reactiveFn; const toggle: typeof _toggle; @@ -53,13 +56,11 @@ globalThis.property = _property; globalThis.assert = _assert; // @ts-ignore global -globalThis.sync = _sync; +globalThis.struct = _struct; // @ts-ignore global globalThis.endpoint = _endpoint; -// // @ts-ignore global -// globalThis.template = _template; // @ts-ignore global -globalThis.jsdoc = _jsdoc; +globalThis.sync = _sync; // can be used instead of import(), calls a DATEX get instruction, works for urls, endpoint, ... export async function get(dx:string|URL|Endpoint, assert_type?:Type | Class | string, context_location?:URL|string, plugins?:string[]):Promise { diff --git a/js_adapter/decorators.ts b/js_adapter/decorators.ts new file mode 100644 index 00000000..568c9590 --- /dev/null +++ b/js_adapter/decorators.ts @@ -0,0 +1,64 @@ +import { endpoint_name, target_clause } from "../datex_all.ts"; +import type { Type } from "../types/type.ts"; +import type { Class } from "../utils/global_types.ts"; +import { Decorators } from "./js_class_adapter.ts"; + + +export function handleClassFieldDecoratorWithOptionalArgs(args:T, context: C|undefined, callback: (arg: T, context: C)=>R): ((value: undefined, context: C) => R)|R { + if (!isDecoratorContext(context)) return (_value: undefined, context: C) => callback(args, context) + else return callback([] as unknown as T, context!) +} +export function handleClassFieldDecoratorWithArgs(args:T, callback: (arg: T, context: C)=>R): ((value: undefined, context: C) => R) { + return (_value: undefined, context: C) => callback(args, context) +} +export function handleClassDecoratorWithOptionalArgs<_Class extends Class, C extends ClassDecoratorContext<_Class>, const T extends unknown[], R>(args:T, value: _Class, context: C|undefined, callback: (arg: T, value: _Class, context: C)=>R): ((value: _Class, context: C) => R)|R { + if (!isDecoratorContext(context)) return (value: _Class, context: C) => callback(args, value, context) + else return callback([] as unknown as T, value, context!) +} +export function handleClassDecoratorWithArgs<_Class extends Class, C extends ClassDecoratorContext<_Class>, const T extends unknown[], R>(args:T, callback: (arg: T, value: _Class, context: C)=>R): ((value: _Class, context: C) => R) { + return (value: _Class, context: C) => callback(args, value, context) +} + + +function isDecoratorContext(context: unknown) { + return context && typeof context === "object" && "kind" in context +} + + + +type PropertyDecoratorContext = ClassFieldDecoratorContext|ClassGetterDecoratorContext|ClassMethodDecoratorContextany)> + +export function property(type: string|Type|Class): (value: ((...args: any[])=>any)|undefined, context: PropertyDecoratorContext)=>void +export function property(value: ((...args: any[])=>any)|undefined, context: PropertyDecoratorContext): void +export function property(type: ((...args: any[])=>any)|undefined|string|Type|Class, context?: PropertyDecoratorContext) { + return handleClassFieldDecoratorWithOptionalArgs([type], context as ClassFieldDecoratorContext, ([type], context:PropertyDecoratorContext) => { + return Decorators.property(type as Type, context) + }) +} + + +export function assert(assertion:(val:T)=>boolean|string|undefined): (value: undefined, context: ClassFieldDecoratorContext)=>void { + return handleClassFieldDecoratorWithArgs([assertion], ([assertion], context) => { + return Decorators.assert(assertion, context) + }) +} + + +export function endpoint(endpoint:target_clause|endpoint_name, scope_name?:string): (value: Class, context: ClassDecoratorContext)=>void +export function endpoint(value: Class, context: ClassDecoratorContext): void +export function endpoint(value: Class|target_clause|endpoint_name, context?: ClassDecoratorContext|string) { + return handleClassDecoratorWithOptionalArgs([value as target_clause|endpoint_name, context as string], value as Class, context as ClassDecoratorContext, ([endpoint, scope_name], value, context) => { + return Decorators.endpoint(endpoint, scope_name, value, context) + }) +} + +/** + * @deprecated Use struct(class {...}) instead; + */ +export function sync(type: string): (value: Class, context: ClassDecoratorContext)=>void +export function sync(value: Class, context: ClassDecoratorContext): void +export function sync(value: Class|string, context?: ClassDecoratorContext) { + return handleClassDecoratorWithOptionalArgs([value as string], value as Class, context as ClassDecoratorContext, ([type], value, context) => { + return Decorators.sync(type, value, context) + }) +} \ No newline at end of file diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index e4a766e7..349b474f 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -16,7 +16,6 @@ import { Runtime, StaticScope } from "../runtime/runtime.ts"; import { Logger } from "../utils/logger.ts"; import { Endpoint, endpoint_name, IdEndpoint, LOCAL_ENDPOINT, Target, target_clause } from "../types/addressing.ts"; -import { context_kind, context_meta_getter, context_meta_setter, context_name } from "./legacy_decorators.ts"; import { Type } from "../types/type.ts"; import { getProxyFunction, getProxyStaticValue, ObjectRef, Pointer, UpdateScheduler } from "../runtime/pointers.ts"; import { Error as DatexError, ValueError } from "../types/errors.ts"; @@ -127,6 +126,19 @@ export class Decorators { static REPLICATOR = Symbol("REPLICATOR"); static DESTRUCTOR = Symbol("DESTRUCTOR"); + + private static setMetadata(context:DecoratorContext, key:string|symbol, value:unknown) { + if (!context.metadata[key]) context.metadata[key] = {}; + const data = context.metadata[key] as {public?:Record, constructor?:any} + if (context.kind == "class") { + data.constructor = value; + } + else { + if (!data.public) data.public = {}; + data.public[context.name] = value; + } + } + /** @expose(allow?:filter): make a method in a static scope available to be called by others */ static public(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[target_clause?] = []) { @@ -166,34 +178,23 @@ export class Decorators { } /** @endpoint(endpoint?:string|Datex.Endpoint, namespace?:string): declare a class as a #public property */ - static endpoint(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[(target_clause|endpoint_name)?, string?] = []) { - - // invalid decorator call - if (!is_static && kind != "class") logger.error("Cannot use @endpoint for non-static field '" + name!.toString() +"'"); - - // handle decorator - else { - - // target endpoint - if (params[0]) { - Decorators.addMetaFilter( - params[0], - setMetadata, getMetadata, Decorators.SEND_FILTER - ) - } - else { - setMetadata(Decorators.SEND_FILTER, true); // indicate to always use local endpoint (expose) - } - - // custom namespace name - setMetadata(Decorators.NAMESPACE, params[1] ?? value?.name) - - // class @endpoint - if (kind == "class") registerPublicStaticClass(value); - - else logger.error("@endpoint can only be used for classes"); - } + static endpoint(endpoint:target_clause|endpoint_name, scope_name:string|undefined, value: Class, context: ClassDecoratorContext) { + // target endpoint + if (endpoint) { + Decorators.addMetaFilter( + endpoint, + context, + Decorators.SEND_FILTER + ) } + else { + this.setMetadata(context, Decorators.SEND_FILTER, true) // indicate to always use local endpoint (expose) + } + + // custom namespace name + this.setMetadata(context, Decorators.NAMESPACE, scope_name ?? value?.name); + registerPublicStaticClass(value, context.metadata); + } /** @root_extension: root extends this static scope in every executed DATEX scope (all static scope members become variables) */ static default(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { @@ -316,31 +317,29 @@ export class Decorators { /** @property: add a field as a template property */ - static property(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[string|number]) { - if (kind != "field" && kind != "getter" && kind != "setter" && kind != "method") logger.error("Invalid use of @property decorator"); + static property(type:string|Type|Class, context: ClassFieldDecoratorContext|ClassGetterDecoratorContext|ClassMethodDecoratorContext) { + if (context.static) { + this.setMetadata(context, Decorators.STATIC_PROPERTY, context.name) + } else { - if (is_static) setMetadata(Decorators.STATIC_PROPERTY, name) - else setMetadata(Decorators.PROPERTY, name) - - // type - if (params?.[0]) { - const type = normalizeType(params[0]); - setMetadata(Decorators.FORCE_TYPE, type) - } + this.setMetadata(context, Decorators.PROPERTY, context.name) } + // type + if (type) { + const normalizedType = normalizeType(type); + this.setMetadata(context, Decorators.FORCE_TYPE, normalizedType) + } } + /** @assert: add type assertion function */ - static assert(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 @assert decorator"); + static assert(assertion: (val:T) => boolean|string|undefined, context: ClassFieldDecoratorContext) { + if (context.static) logger.error("Cannot use @assert with static fields"); else { - if (typeof params[0] !== "function") logger.error("Invalid @assert decorator value, must be a function"); - else { - const assertionType = new Conjunction(Assertion.get(undefined, params[0], false)); - setMetadata(Decorators.FORCE_TYPE, assertionType) - } + const assertionType = new Conjunction(Assertion.get(undefined, assertion, false)); + this.setMetadata(context, Decorators.FORCE_TYPE, assertionType) } } @@ -404,44 +403,32 @@ export class Decorators { } /** @sync: sync class/property */ - static sync(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[(string|Type)?] = []) { + static sync(type: string|Type, value: Class, context: ClassDecoratorContext) { - // invalid decorator call - if (is_static) logger.error("Cannot use @sync for static field '" + name.toString() +"'"); - if (is_static) logger.error("Cannot use @sync for static field '" + name.toString() +"'"); + this.setMetadata(context, Decorators.IS_SYNC, true) - // handle decorator - else { - setMetadata(Decorators.IS_SYNC, true) + const originalClass = value; - // is auto sync class -> create class proxy (like in template) - if (kind == "class") { - //initPropertyTypeAssigner(); + let normalizedType: Type; - const original_class = value; - let type: Type; - - // get template type - if (typeof params[0] == "string" || params[0] instanceof Type) { - 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.replace(/^_/, '')); // remove leading _ from type name + // get template type + if (typeof type == "string" || type instanceof Type) { + normalizedType = normalizeType(type, false, "ext"); + } + else if (originalClass[METADATA]?.[Decorators.FORCE_TYPE]?.constructor) normalizedType = originalClass[METADATA]?.[Decorators.FORCE_TYPE]?.constructor + else normalizedType = Type.get("ext", originalClass.name.replace(/^_/, '')); // remove leading _ from type name - let callerFile:string|undefined; + let callerFile:string|undefined; - if (client_type == "deno" && type.namespace !== "std") { - callerFile = getCallerInfo()?.[2]?.file ?? undefined; - if (!callerFile) { - logger.error("Could not determine JS module URL for type '" + type + "'") - } - } - - // return new templated class - return createTemplateClass(original_class, type, true, true, callerFile); + if (client_type == "deno" && normalizedType.namespace !== "std") { + callerFile = getCallerInfo()?.[2]?.file ?? undefined; + if (!callerFile) { + logger.error("Could not determine JS module URL for type '" + normalizedType + "'") } - } + + // return new templated class + return createTemplateClass(originalClass, normalizedType, true, true, callerFile, context.metadata); } /** @sealed: sealed class/property */ @@ -560,14 +547,9 @@ export class Decorators { // handle ALLOW_FILTER for classes, methods and fields // adds filter - private static addMetaFilter(new_filter:target_clause|endpoint_name, setMetadata:context_meta_setter, getMetadata:context_meta_getter, filter_symbol:symbol){ - // // create filter if not existing - // let filter:Filter = getMetadata(filter_symbol) - // if (!filter) {filter = new Filter(); setMetadata(filter_symbol, filter)} - // filter.appendFilter(new_filter); - - if (typeof new_filter == "string") setMetadata(filter_symbol, Target.get(new_filter)) - else setMetadata(filter_symbol, new_filter) + private static addMetaFilter(new_filter:target_clause|endpoint_name, context: DecoratorContext, filter_symbol:symbol){ + if (typeof new_filter == "string") this.setMetadata(context, filter_symbol, Target.get(new_filter)) + else this.setMetadata(context, filter_symbol, new_filter) } } @@ -608,55 +590,55 @@ function normalizeType(type:Type|string|Class, allowTypeParams = true, defaultNa const initialized_static_scope_classes = new Map(); -const registered_static_classess = new Set(); -function registerPublicStaticClass(original_class:Class){ - registered_static_classess.add(original_class); +const registered_static_classes = new Set(); +function registerPublicStaticClass(publicClass:Class, metadata?:Record){ + registered_static_classes.add(publicClass); - // if endpoint already loaded, init class - initPublicStaticClasses() + // init class (if endpoint already loaded) + initPublicStaticClass(publicClass, metadata) } type class_data = {name:string, static_scope:StaticScope, properties: string[], metadata:any} -export function initPublicStaticClasses(){ - if (!Runtime.endpoint || Runtime.endpoint === LOCAL_ENDPOINT) return; - - for (const reg_class of registered_static_classess) { - - if (initialized_static_scope_classes.has(reg_class)) continue; // already initialized +export function initPublicStaticClasses(){ + for (const reg_class of registered_static_classes) { + initPublicStaticClass(reg_class) + } +} - const metadata = (reg_class)[METADATA]; - let targets = metadata[Decorators.SEND_FILTER]?.constructor; - if (targets == true) targets = Runtime.endpoint; // use own endpoint per default +function initPublicStaticClass(publicClass: Class, metadata?:Record) { + if (!Runtime.endpoint || Runtime.endpoint === LOCAL_ENDPOINT) return; + if (initialized_static_scope_classes.has(publicClass)) return; - let data:any; + metadata ??= (publicClass)[METADATA]; + if (!metadata) throw new Error(`Missing metadata for class ${publicClass.name}`) + let targets = metadata[Decorators.SEND_FILTER]?.constructor; + if (targets == true) targets = Runtime.endpoint; // use own endpoint per default - // expose if current endpoint matches class endpoint - if (Logical.matches(Runtime.endpoint, targets, Target)) { - data ??= getStaticClassData(reg_class); - if (!data) throw new Error("Could not get data for static class") - exposeStaticClass(reg_class, data); - } - - // also enable remote access if not exactly and only the current endpoint - if (Runtime.endpoint !== targets) { - data ??= getStaticClassData(reg_class, false); - if (!data) throw new Error("Could not get data for static class") - remoteStaticClass(reg_class, data, targets) - } + let data:any; + + // expose if current endpoint matches class endpoint + if (Logical.matches(Runtime.endpoint, targets, Target)) { + data ??= getStaticClassData(publicClass, true, metadata); + if (!data) throw new Error("Could not get data for static class") + exposeStaticClass(publicClass, data); + } - - DatexObject.seal(data.static_scope); - initialized_static_scope_classes.set(reg_class, data.static_scope); + // also enable remote access if not exactly and only the current endpoint + if (Runtime.endpoint !== targets) { + data ??= getStaticClassData(publicClass, false, metadata); + if (!data) throw new Error("Could not get data for static class") + remoteStaticClass(publicClass, data, targets) } + + DatexObject.seal(data.static_scope); + initialized_static_scope_classes.set(publicClass, data.static_scope); } function exposeStaticClass(original_class:Class, data:class_data) { - // console.log("expose class", data, data.metadata[Decorators.STATIC_PROPERTY]); - const exposed_public = data.metadata[Decorators.STATIC_PROPERTY]?.public; const exposed_private = data.metadata[Decorators.STATIC_PROPERTY]?.private; @@ -789,8 +771,8 @@ function remoteStaticClass(original_class:Class, data:class_data, targets:target } -function getStaticClassData(original_class:Class, staticScope = true) { - const metadata = (original_class)[METADATA]; +function getStaticClassData(original_class:Class, staticScope = true, metadata?:Record) { + metadata ??= (original_class)[METADATA]; if (!metadata) return; const static_scope_name = typeof metadata[Decorators.NAMESPACE]?.constructor == 'string' ? metadata[Decorators.NAMESPACE]?.constructor : original_class.name; const static_properties = Object.getOwnPropertyNames(original_class) @@ -1058,9 +1040,9 @@ function _old_publicStaticClass(original_class:Class) { const templated_classes = new Map() // original class, templated class -export function createTemplateClass(original_class:{ new(...args: any[]): any; }, type:Type, sync = true, add_js_interface = true, callerFile?:string){ +export function createTemplateClass(original_class:{ new(...args: any[]): any; }, type:Type, sync = true, add_js_interface = true, callerFile?:string, metadata?:Record){ - if (templated_classes.has(original_class)) return templated_classes.get(original_class); + if (templated_classes.has(original_class)) return templated_classes.get(original_class)!; original_class[DX_TYPE] = type; @@ -1077,19 +1059,20 @@ export function createTemplateClass(original_class:{ new(...args: any[]): any; } type.jsTypeDefModule = callerFile; } + metadata ??= original_class.prototype[METADATA]; // set constructor, replicator, destructor - const constructor_name = Object.keys(original_class.prototype[METADATA]?.[Decorators.CONSTRUCTOR]?.public??{})[0] - const replicator_name = Object.keys(original_class.prototype[METADATA]?.[Decorators.REPLICATOR]?.public??{})[0] - const destructor_name = Object.keys(original_class.prototype[METADATA]?.[Decorators.DESTRUCTOR]?.public??{})[0] + const constructor_name = original_class.prototype['construct'] ? 'construct' : null; // Object.keys(metadata?.[Decorators.CONSTRUCTOR]?.public??{})[0] + const replicator_name = original_class.prototype['construct'] ? 'replicate' : null; // Object.keys(metadata?.[Decorators.REPLICATOR]?.public??{})[0] + const destructor_name = Object.keys(metadata?.[Decorators.DESTRUCTOR]?.public??{})[0] if (constructor_name) type.setConstructor(original_class.prototype[constructor_name]); if (replicator_name) type.setReplicator(original_class.prototype[replicator_name]); if (destructor_name) type.setDestructor(original_class.prototype[destructor_name]); // set template - const property_types = original_class.prototype[METADATA]?.[Decorators.FORCE_TYPE]?.public; - const allow_filters = original_class.prototype[METADATA]?.[Decorators.ALLOW_FILTER]?.public; + const property_types = metadata?.[Decorators.FORCE_TYPE]?.public; + const allow_filters = metadata?.[Decorators.ALLOW_FILTER]?.public; const template = {}; template[DX_PERMISSIONS] = {} @@ -1109,7 +1092,7 @@ export function createTemplateClass(original_class:{ new(...args: any[]): any; } // iterate over all properties TODO different dx_name? - for (const [name, dx_name] of Object.entries(original_class.prototype[METADATA]?.[Decorators.PROPERTY]?.public??{})) { + for (const [name, dx_name] of Object.entries(metadata?.[Decorators.PROPERTY]?.public??{})) { let metadataConstructor = MetadataReflect.getMetadata && MetadataReflect.getMetadata("design:type", original_class.prototype, name); // if type is Object -> std:Any if (metadataConstructor == Object) metadataConstructor = null; @@ -1125,7 +1108,7 @@ export function createTemplateClass(original_class:{ new(...args: any[]): any; } _old_publicStaticClass(original_class); // create shadow class extending the actual class - const sync_auto_cast_class = proxyClass(original_class, type, original_class[METADATA]?.[Decorators.IS_SYNC]?.constructor ?? sync) + const sync_auto_cast_class = proxyClass(original_class, type, metadata?.[Decorators.IS_SYNC]?.constructor ?? sync) // only for debugging / dev console TODO remove globalThis[sync_auto_cast_class.name] = sync_auto_cast_class; @@ -1135,27 +1118,6 @@ export function createTemplateClass(original_class:{ new(...args: any[]): any; } return sync_auto_cast_class; } -// TODO each -// if (is_each) { - -// // call _e - -// const static_scope_name = original_class[METADATA]?.[Decorators.SCOPE_NAME]?.constructor ?? original_class.name -// let filter:DatexFilter; // contains all endpoints that have the pointer - -// Object.defineProperty(instance, p, {value: async function(...args:any[]) { -// if (!filter) { -// let ptr = DatexPointer.getByValue(this); -// if (!(ptr instanceof DatexPointer)) throw new DatexError("called @each method on non-pointer"); -// filter = DatexFilter.OR(await ptr.getSubscribersFilter(), ptr.origin); -// } -// console.log("all endpoints filter: " + filter); - -// return DatexRuntime.datexOut([`--static.${static_scope_name}._e.${p} ?`, [new DatexTuple(this, ...args)], {to:filter, sign:true}], filter); -// }}) -// } - - // Reflect metadata / decorator metadata, get parameters & types if available function getMethodParams(target:Function, method_name:string, meta_param_index?:number):Tuple{ @@ -1221,50 +1183,26 @@ function normalizeFunctionParams(params: string) { } -// let _assigner_init = false; -// function initPropertyTypeAssigner(){ -// if (_assigner_init) return; -// _assigner_init = true; -// // TODO just a workaround, handle PropertyTypeAssigner different (currently nodejs error!! DatexPointer not yet defined) -// Pointer.setPropertyTypeAssigner({getMethodMetaParamIndex:getMetaParamIndex, getMethodParams:getMethodParams}) -// } -//Pointer.setPropertyTypeAssigner({getMethodMetaParamIndex:getMetaParamIndex, getMethodParams:getMethodParams}) - DatexFunction.setMethodParamsSource(getMethodParams) DatexFunction.setMethodMetaIndexSource(getMetaParamIndex) -/** @meta: mark meta parameter in a datex method with @meta */ -// export function meta(target: Object, propertyKey: string | symbol, parameterIndex: number) { -// Reflect.defineMetadata( -// "unyt:meta", -// parameterIndex, -// target, -// propertyKey -// ); -// } - // 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 unknown) = (new (...args: unknown[]) => unknown), Construct = InstanceType["construct"]> { +export interface DatexClass unknown) = (new (...args: unknown[]) => unknown), Construct = InstanceType["construct" & keyof InstanceType]> { 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; - options: (options:any)=>T; - - // decorator equivalents - to: (target:target_clause) => dc; - no_result: () => dc; - - // @sync objects - is_origin?: boolean; - origin_id?: number; - room_id?: number; } -type dc&{new (...args:unknown[]):unknown}> = DatexClass & T & ((struct:InstanceType) => datexClassType); +export type MethodKeys = { + [K in keyof T]: T[K] extends (...args: any) => any ? K : never; +}[keyof T]; + +export type dc&{new (...args:unknown[]):unknown}, OT extends {new (...args:unknown[]):unknown} = ObjectRef> = + DatexClass & + Pick & + ((struct:Omit, MethodKeys>>) => datexClassType); /** * Workaround to enable correct @sync class typing, until new decorators support it. diff --git a/mod.ts b/mod.ts index cbc7e9fc..661ca2c0 100644 --- a/mod.ts +++ b/mod.ts @@ -18,7 +18,7 @@ import * as Datex from "./datex_all.ts"; export {Datex}; -export * from "./js_adapter/legacy_decorators.ts"; +export * from "./js_adapter/decorators.ts"; export * from "./datex_short.ts"; export {init} from "./init.ts"; diff --git a/types/addressing.ts b/types/addressing.ts index 136ed104..d5ac1aaa 100644 --- a/types/addressing.ts +++ b/types/addressing.ts @@ -43,6 +43,7 @@ export type endpoints = Endpoint|Disjunction export class Target implements ValueConsumer { + // TODO: remove entry when Endpoint WeakRef was garbage collected protected static targets = new Map>(); // target string -> target element static readonly prefix:target_prefix = "@" static readonly type:BinaryCode diff --git a/types/logic.ts b/types/logic.ts index 13dd178b..b6879b6f 100644 --- a/types/logic.ts +++ b/types/logic.ts @@ -155,7 +155,9 @@ export class Logical extends Set { // wrong atomic type at runtime // guard for: against is T if (!(against instanceof atomic_class)) { - throw new RuntimeError(`Invalid match check: atomic value has wrong type (expected ${Type.getClassDatexType(atomic_class)}, found ${Type.ofValue(against)})`); + console.error(`Invalid match check: atomic value has wrong type (expected ${Type.getClassDatexType(atomic_class)}, found ${Type.ofValue(against)})`) + // throw new RuntimeError(`Invalid match check: atomic value has wrong type (expected ${Type.getClassDatexType(atomic_class)}, found ${Type.ofValue(against)})`); + return true; } // match diff --git a/types/struct.ts b/types/struct.ts index a4d68077..1ce9e1a9 100644 --- a/types/struct.ts +++ b/types/struct.ts @@ -1,5 +1,7 @@ +import { dc } from "../js_adapter/js_class_adapter.ts"; import type { ObjectRef } from "../runtime/pointers.ts"; import { Runtime } from "../runtime/runtime.ts"; +import { Class } from "../utils/global_types.ts"; import { sha256 } from "../utils/sha256.ts"; import { Type } from "./type.ts"; @@ -19,7 +21,12 @@ type collapseType = { ) } -export type inferType = DXType extends Type ? Def : never; +export type inferType = + DXTypeOrClass extends Type ? + ObjectRef : + DXTypeOrClass extends Class ? + InstanceType : + never; /** * Define a structural type without a class or prototype. @@ -57,10 +64,17 @@ export type inferType = DXType extends Type ? De * ``` */ - -export function struct(def: Def): Type> & ((val: collapseType)=>ObjectRef>) { +export function struct & Class>(classDefinition: T): dc +export function struct(def: Def): Type> & ((val: collapseType)=>ObjectRef>) +export function struct(def: StructuralTypeDefIn|Class): any { // create unique type name from template hash + // is class definition + if (typeof def == "function") { + throw new Error("todo struct class") + } + + // is struct definition if (!def || typeof def !== "object") throw new Error("Struct definition must of type object"); const template:StructuralTypeDef = {}; diff --git a/utils/global_types.ts b/utils/global_types.ts index 045649b7..2c877137 100644 --- a/utils/global_types.ts +++ b/utils/global_types.ts @@ -201,7 +201,7 @@ export type datex_scope = { closed?: boolean // is scope completely closed? } -export type Class = (new (...args: any[]) => T); // type for a JS class +export type Class = (new (...args: any[]) => T) | (abstract new (...args: any[]) => T); // type for a JS class export type compile_info = [datex:string|PrecompiledDXB, data?:any[], options?:compiler_options, add_header?:boolean, is_child_scope_block?:boolean, extract_pointers?:boolean, save_precompiled?:PrecompiledDXB, max_block_size?:number]; From ae20a0805189414d72967f24ca399c81737bca70 Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 27 Feb 2024 22:19:37 +0100 Subject: [PATCH 28/56] fix structs --- js_adapter/js_class_adapter.ts | 31 +++++++++++++++++++------------ types/struct.ts | 13 ++++++++++--- utils/global_types.ts | 2 +- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index 349b474f..8ae06573 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -127,8 +127,8 @@ export class Decorators { static DESTRUCTOR = Symbol("DESTRUCTOR"); - private static setMetadata(context:DecoratorContext, key:string|symbol, value:unknown) { - if (!context.metadata[key]) context.metadata[key] = {}; + public static setMetadata(context:DecoratorContext, key:string|symbol, value:unknown) { + if (!context.metadata[key]) context.metadata[key] = {} const data = context.metadata[key] as {public?:Record, constructor?:any} if (context.kind == "class") { data.constructor = value; @@ -403,9 +403,11 @@ export class Decorators { } /** @sync: sync class/property */ - static sync(type: string|Type, value: Class, context: ClassDecoratorContext) { + static sync(type: string|Type|undefined, value: Class, context?: ClassDecoratorContext) { - this.setMetadata(context, Decorators.IS_SYNC, true) + if (context) { + this.setMetadata(context ?? {kind: "class", metadata:(value as any)[METADATA]}, Decorators.IS_SYNC, true) + } const originalClass = value; @@ -415,7 +417,10 @@ export class Decorators { if (typeof type == "string" || type instanceof Type) { normalizedType = normalizeType(type, false, "ext"); } - else if (originalClass[METADATA]?.[Decorators.FORCE_TYPE]?.constructor) normalizedType = originalClass[METADATA]?.[Decorators.FORCE_TYPE]?.constructor + else if ( + originalClass[METADATA]?.[Decorators.FORCE_TYPE] && + Object.hasOwn(originalClass[METADATA]?.[Decorators.FORCE_TYPE], 'constructor') + ) normalizedType = originalClass[METADATA]?.[Decorators.FORCE_TYPE]?.constructor else normalizedType = Type.get("ext", originalClass.name.replace(/^_/, '')); // remove leading _ from type name let callerFile:string|undefined; @@ -428,7 +433,7 @@ export class Decorators { } // return new templated class - return createTemplateClass(originalClass, normalizedType, true, true, callerFile, context.metadata); + return createTemplateClass(originalClass, normalizedType, true, true, callerFile, context?.metadata); } /** @sealed: sealed class/property */ @@ -986,7 +991,7 @@ function _old_publicStaticClass(original_class:Class) { // each methods //const each_private = original_class.prototype[METADATA]?.[Decorators.IS_EACH]?.private; - const each_public = original_class.prototype[METADATA]?.[Decorators.IS_EACH]?.public; + const each_public = original_class[METADATA]?.[Decorators.IS_EACH]?.public; let each_scope: any; @@ -1040,7 +1045,7 @@ function _old_publicStaticClass(original_class:Class) { const templated_classes = new Map() // original class, templated class -export function createTemplateClass(original_class:{ new(...args: any[]): any; }, type:Type, sync = true, add_js_interface = true, callerFile?:string, metadata?:Record){ +export function createTemplateClass(original_class: Class, type:Type, sync = true, add_js_interface = true, callerFile?:string, metadata?:Record){ if (templated_classes.has(original_class)) return templated_classes.get(original_class)!; @@ -1059,12 +1064,12 @@ export function createTemplateClass(original_class:{ new(...args: any[]): any; } type.jsTypeDefModule = callerFile; } - metadata ??= original_class.prototype[METADATA]; + metadata ??= original_class[METADATA]; // set constructor, replicator, destructor const constructor_name = original_class.prototype['construct'] ? 'construct' : null; // Object.keys(metadata?.[Decorators.CONSTRUCTOR]?.public??{})[0] - const replicator_name = original_class.prototype['construct'] ? 'replicate' : null; // Object.keys(metadata?.[Decorators.REPLICATOR]?.public??{})[0] - const destructor_name = Object.keys(metadata?.[Decorators.DESTRUCTOR]?.public??{})[0] + const replicator_name = original_class.prototype['replicate'] ? 'replicate' : null; // Object.keys(metadata?.[Decorators.REPLICATOR]?.public??{})[0] + const destructor_name = original_class.prototype['destruct'] ? 'destruct' : null; // Object.keys(metadata?.[Decorators.DESTRUCTOR]?.public??{})[0] if (constructor_name) type.setConstructor(original_class.prototype[constructor_name]); if (replicator_name) type.setReplicator(original_class.prototype[replicator_name]); @@ -1201,7 +1206,9 @@ export type MethodKeys = { export type dc&{new (...args:unknown[]):unknown}, OT extends {new (...args:unknown[]):unknown} = ObjectRef> = DatexClass & - Pick & + OT & + // TODO: required instead of OT to disable default constructor, but leads to problems with typing + // Pick & ((struct:Omit, MethodKeys>>) => datexClassType); /** diff --git a/types/struct.ts b/types/struct.ts index 1ce9e1a9..c7bc2f0b 100644 --- a/types/struct.ts +++ b/types/struct.ts @@ -4,6 +4,7 @@ import { Runtime } from "../runtime/runtime.ts"; import { Class } from "../utils/global_types.ts"; import { sha256 } from "../utils/sha256.ts"; import { Type } from "./type.ts"; +import { Decorators } from "../js_adapter/js_class_adapter.ts"; type StructuralTypeDefIn = { [key: string]: Type|(new () => unknown)|StructuralTypeDefIn @@ -65,13 +66,19 @@ export type inferType = */ export function struct & Class>(classDefinition: T): dc +export function struct & Class>(type: string, classDefinition: T): dc +export function struct(typeName: string, def: Def): Type> & ((val: collapseType)=>ObjectRef>) export function struct(def: Def): Type> & ((val: collapseType)=>ObjectRef>) -export function struct(def: StructuralTypeDefIn|Class): any { +export function struct(defOrTypeName: StructuralTypeDefIn|Class|string, def?: StructuralTypeDefIn|Class): any { // create unique type name from template hash + const hasType = typeof defOrTypeName == "string"; + const typeName = hasType ? defOrTypeName : undefined; + def = hasType ? def : defOrTypeName; + // is class definition if (typeof def == "function") { - throw new Error("todo struct class") + return Decorators.sync(typeName, def); } // is struct definition @@ -98,7 +105,7 @@ export function struct(def: StructuralTypeDefIn|Class): any { } } - const hash = sha256(Runtime.valueToDatexStringExperimental(template)) + const hash = typeName ?? sha256(Runtime.valueToDatexStringExperimental(template)) const type = new Type("struct", hash).setTemplate(template); type.proxify_children = true; return type as any diff --git a/utils/global_types.ts b/utils/global_types.ts index 2c877137..045649b7 100644 --- a/utils/global_types.ts +++ b/utils/global_types.ts @@ -201,7 +201,7 @@ export type datex_scope = { closed?: boolean // is scope completely closed? } -export type Class = (new (...args: any[]) => T) | (abstract new (...args: any[]) => T); // type for a JS class +export type Class = (new (...args: any[]) => T); // type for a JS class export type compile_info = [datex:string|PrecompiledDXB, data?:any[], options?:compiler_options, add_header?:boolean, is_child_scope_block?:boolean, extract_pointers?:boolean, save_precompiled?:PrecompiledDXB, max_block_size?:number]; From ddbddbded241a9043c0678d5cb83a8578f43aa5c Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 28 Feb 2024 00:17:38 +0100 Subject: [PATCH 29/56] sql optimizations, multiqueries --- storage/storage-locations/sql-db.ts | 159 +++++++++++++++++++++------- 1 file changed, 118 insertions(+), 41 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 29ba8514..50ab014c 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -86,13 +86,16 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } satisfies Record; // cached table columns - #tableColumns = new Map>() + #tableColumns = new Map>() // cached table -> type mapping #tableTypes = new Map() #existingItemsCache = new Set() #existingPointersCache = new Set() + // remember tables for pointers that still need to be loaded + #pointerTables = new Map() + constructor(options:dbOptions, private log?:(...args:unknown[])=>void) { super() this.#options = options @@ -275,7 +278,9 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (!type.template) throw new Error("Cannot create table for non-templated type " + type) const snakeCaseName = type.name.replace(/([A-Z])/g, "_$1").toLowerCase().slice(1).replace(/__+/g, '_'); const snakeCasePlural = snakeCaseName + (snakeCaseName.endsWith("s") ? "es" : "s"); - return type.namespace=="ext" ? snakeCasePlural : `${type.namespace}_${snakeCasePlural}`; + const name = type.namespace=="ext"||type.namespace=="struct" ? snakeCasePlural : `${type.namespace}_${snakeCasePlural}`; + if (name.length > 64) throw new Error("Type name too long: " + type); + else return name; } #typeToString(type: Datex.Type) { @@ -385,7 +390,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async #getTableColumns(tableName: string) { if (!this.#tableColumns.has(tableName)) { - const columnData = new Map() + const columnData = new Map() const columns = await this.#query<{COLUMN_NAME:string, COLUMN_KEY:string, DATA_TYPE:string}>( new Query() .table("information_schema.columns") @@ -395,9 +400,22 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .build() ) + const constraints = (await this.#query<{COLUMN_NAME:string, REFERENCED_TABLE_NAME:string}>( + new Query() + .table("information_schema.key_column_usage") + .select("COLUMN_NAME", "REFERENCED_TABLE_NAME") + .where(Where.eq("table_schema", this.#options.db)) + .where(Where.eq("table_name", tableName)) + .build() + )); + const columnTables = new Map() + for (const {COLUMN_NAME, REFERENCED_TABLE_NAME} of constraints) { + columnTables.set(COLUMN_NAME, REFERENCED_TABLE_NAME) + } + for (const col of columns) { if (col.COLUMN_NAME == this.#pointerMysqlColumnName) continue; - columnData.set(col.COLUMN_NAME, {foreignPtr: col.COLUMN_KEY == "MUL", type: col.DATA_TYPE}) + columnData.set(col.COLUMN_NAME, {foreignPtr: columnTables.has(col.COLUMN_NAME), foreignTable: columnTables.get(col.COLUMN_NAME), type: col.DATA_TYPE}) } this.#tableColumns.set(tableName, columnData) @@ -540,26 +558,55 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return `${type.toString()} ${objectString}` } + #templateMultiQueries = new Map, result: Promise[]>}>() + async #getTemplatedPointerObject(pointerId: string, table?: string) { table = table ?? await this.#getPointerTable(pointerId); if (!table) { logger.error("No table found for pointer " + pointerId); return null; } - const object = await this.#queryFirst>( - new Query() - .table(table) - .select("*") - .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) - .build() - ) - if (!object) return null; - const type = await this.#getTypeForTable(table); - if (!type) { - logger.error("No type found for table " + table); - return null; + + let result: Promise[]>; + + if (this.#templateMultiQueries.has(table)) { + const multiQuery = this.#templateMultiQueries.get(table)! + multiQuery.pointers.add(pointerId) + result = multiQuery.result; + } + else { + const pointers = new Set([pointerId]) + result = (async () => { + await sleep(200); + this.#templateMultiQueries.delete(table) + return this.#query>( + new Query() + .table(table) + .select("*", this.#pointerMysqlColumnName) + .where(Where.in(this.#pointerMysqlColumnName, Array.from(pointers))) + .build() + ) + })() + this.#templateMultiQueries.set(table, {pointers, result}) } - return object; + + return (await result). + find(obj => obj[this.#pointerMysqlColumnName] == pointerId); + + // const object = await this.#queryFirst>( + // new Query() + // .table(table) + // .select("*") + // .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) + // .build() + // ) + // if (!object) return null; + // const type = await this.#getTypeForTable(table); + // if (!type) { + // logger.error("No type found for table " + table); + // return null; + // } + // return object; } async #getTemplatedPointerValueDXB(pointerId: string, table?: string) { @@ -570,6 +617,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #getPointerTable(pointerId: string) { + if (this.#pointerTables.has(pointerId)) { + const table = this.#pointerTables.get(pointerId); + this.#pointerTables.delete(pointerId); + return table; + } return (await this.#queryFirst<{table_name:string}>( new Query() .table(this.#metaTables.pointerMapping.name) @@ -651,6 +703,9 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { async matchQuery(itemPrefix: string, valueType: Datex.Type, match: Datex.MatchInput, options: Options): Promise> { + // measure total query time + const start = Date.now(); + const joins = new Map() const collectedTableTypes = new Set([valueType]) const collectedIdentifiers = new Set() @@ -746,6 +801,13 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // TODO: atomic operations for multiple queries const {foundRows} = (options?.returnAdvanced ? await this.#queryFirst<{foundRows: number}>("SELECT FOUND_ROWS() as foundRows") : null) ?? {foundRows: -1} + // remember pointer table + for (const ptrId of ptrIds) { + this.#pointerTables.set(ptrId, rootTableName) + } + + const loadStart = Date.now(); + const result = new Set((await Promise.all(limitedPtrIds.map(ptrId => Pointer.load(ptrId)))).filter(ptr => { if (ptr instanceof LazyPointer) { logger.warn("Cannot return lazy pointer from match query (" + ptr.id + ")"); @@ -754,6 +816,9 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return true; }).map(ptr => (ptr as Pointer).val as T)) + console.log("load time", (Date.now() - loadStart) + "ms") + console.log("total query time", (Date.now() - start) + "ms") + if (options?.returnAdvanced) { return { matches: result, @@ -1145,6 +1210,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) .build() ))?.value; + if (value) this.#existingPointersCache.add(pointerId); return value ? Runtime.decodeValue(this.#stringToBinary(value), outer_serialized) : NOT_EXISTING; } @@ -1169,6 +1235,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { else if (value_pointer != undefined) result.add(await Pointer.load(value_pointer)) else if (value_dxb != undefined) result.add(await Runtime.decodeValue(this.#stringToBinary(value_dxb))) } + this.#existingPointersCache.add(pointerId); return result; } @@ -1185,34 +1252,44 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // resolve foreign pointers const columns = await this.#getTableColumns(table); if (!columns) throw new Error("No columns found for table " + table) - for (const [colName, {foreignPtr, type}] of columns.entries()) { - - // custom conversions: - // convert blob strings to ArrayBuffer - if (type == "blob" && typeof object[colName] == "string") { - object[colName] = this.#stringToBinary(object[colName] as string) - } - // convert Date ot Time - else if (object[colName] instanceof Date) { - object[colName] = new Time(object[colName] as Date) - } - - // is an object type with a template - if (foreignPtr) { - if (typeof object[colName] == "string") { - object[colName] = await Pointer.load(object[colName] as string); - } - // else property is null/undefined - } - // is blob, assume it is a DXB value - else if (type == "blob") { - object[colName] = await Runtime.decodeValue(object[colName] as ArrayBuffer, true); - } - } + + await Promise.all( + [...columns.entries()] + .map(([colName, {foreignPtr, foreignTable, type}]) => this.assignPointerProperty(object, colName, type, foreignPtr, foreignTable)) + ) + + + this.#existingPointersCache.add(pointerId); return type.cast(object, undefined, undefined, false); } } + private async assignPointerProperty(object:Record, colName:string, type:string, foreignPtr:boolean, foreignTable?:string) { + // custom conversions: + // convert blob strings to ArrayBuffer + if (type == "blob" && typeof object[colName] == "string") { + object[colName] = this.#stringToBinary(object[colName] as string) + } + // convert Date ot Time + else if (object[colName] instanceof Date) { + object[colName] = new Time(object[colName] as Date) + } + + // is an object type with a template + if (foreignPtr) { + if (typeof object[colName] == "string") { + const ptrId = object[colName] as string; + if (foreignTable) this.#pointerTables.set(ptrId, foreignTable) + object[colName] = await Pointer.load(ptrId); + } + // else property is null/undefined + } + // is blob, assume it is a DXB value + else if (type == "blob") { + object[colName] = await Runtime.decodeValue(object[colName] as ArrayBuffer, true); + } + } + async removePointer(pointerId: string): Promise { From 8c58dd49fd042ae8585b01ab38892c8aca1d734b Mon Sep 17 00:00:00 2001 From: benStre Date: Wed, 28 Feb 2024 00:18:02 +0100 Subject: [PATCH 30/56] sql optimizations, multiqueries --- storage/storage-locations/sql-db.ts | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 50ab014c..9dd3dd80 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -95,6 +95,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // remember tables for pointers that still need to be loaded #pointerTables = new Map() + #templateMultiQueries = new Map, result: Promise[]>}>() constructor(options:dbOptions, private log?:(...args:unknown[])=>void) { super() @@ -558,7 +559,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return `${type.toString()} ${objectString}` } - #templateMultiQueries = new Map, result: Promise[]>}>() async #getTemplatedPointerObject(pointerId: string, table?: string) { table = table ?? await this.#getPointerTable(pointerId); @@ -592,21 +592,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { return (await result). find(obj => obj[this.#pointerMysqlColumnName] == pointerId); - - // const object = await this.#queryFirst>( - // new Query() - // .table(table) - // .select("*") - // .where(Where.eq(this.#pointerMysqlColumnName, pointerId)) - // .build() - // ) - // if (!object) return null; - // const type = await this.#getTypeForTable(table); - // if (!type) { - // logger.error("No type found for table " + table); - // return null; - // } - // return object; } async #getTemplatedPointerValueDXB(pointerId: string, table?: string) { From 8dce9ee4391734123699f32822057f0a859a41c2 Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 29 Feb 2024 00:00:07 +0100 Subject: [PATCH 31/56] update wasm decompiler (unrelated) --- wasm/adapter/Cargo.lock | 16 ++++++++++++++++ wasm/adapter/pkg/datex_wasm_bg.wasm | Bin 805984 -> 829979 bytes 2 files changed, 16 insertions(+) diff --git a/wasm/adapter/Cargo.lock b/wasm/adapter/Cargo.lock index c9927f04..3dc44a63 100644 --- a/wasm/adapter/Cargo.lock +++ b/wasm/adapter/Cargo.lock @@ -187,6 +187,7 @@ dependencies = [ "num-bigint", "num-integer", "num_enum", + "pad", "pest", "pest_derive", "regex", @@ -660,6 +661,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "pad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width", +] + [[package]] name = "parking_lot" version = "0.9.0" @@ -1314,6 +1324,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "url" version = "1.7.2" diff --git a/wasm/adapter/pkg/datex_wasm_bg.wasm b/wasm/adapter/pkg/datex_wasm_bg.wasm index ae7dfda09ddca92eb7b541a0f9a669ee8b6d020a..ea316a350a5f7efd7b6d628bec5b5e76988e688c 100644 GIT binary patch delta 206882 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%0ALN_GLdg*}`057c3r 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 z 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

    HMA|)qevP)p!NSpKj0>D~%EQEcV>_xDvWiN(( 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 delta 185380 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

    1o#qUKwky?41bH+rXabZV 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 From 6e9397c90cbcb92dedf4b4c1677368a5e78b4cfd Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 29 Feb 2024 00:25:20 +0100 Subject: [PATCH 32/56] improve sql storage, match queries --- storage/storage-locations/sql-db.ts | 132 +++++++++++++++++++++++----- storage/storage.ts | 5 ++ types/storage-map.ts | 1 - types/storage-set.ts | 1 - utils/match.ts | 5 +- 5 files changed, 117 insertions(+), 27 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index 9dd3dd80..ff843d00 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -22,6 +22,9 @@ import { MatchOptions, MatchCondition, MatchConditionType, ComputedProperty, Com import { MatchResult } from "../storage.ts"; import { Time } from "../../types/time.ts"; import { Order } from "https://deno.land/x/sql_builder@v1.9.2/order.ts"; +import { configLogger } from "https://deno.land/x/mysql@v2.12.1/src/logger.ts"; + +configLogger({level: "WARNING"}) const logger = new Logger("SQL Storage"); @@ -92,6 +95,8 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { #existingItemsCache = new Set() #existingPointersCache = new Set() + #tableCreationTasks = new Map>() + #tableColumnTasks = new Map>>() // remember tables for pointers that still need to be loaded #pointerTables = new Map() @@ -103,8 +108,8 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #connect(){ if (this.#connected) return; - this.#sqlClient = await new Client().connect(this.#options); - this.log?.("Connected to SQL database " + this.#options.db + " on " + this.#options.hostname + ":" + this.#options.port) + this.#sqlClient = await new Client().connect({poolSize: 30, ...this.#options}); + logger.info("Using SQL database " + this.#options.db + " on " + this.#options.hostname + ":" + this.#options.port + " as storage location") this.#connected = true; } @@ -143,10 +148,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } // truncate meta tables - for (const table of Object.values(this.#metaTables)) { - await this.#query<{table_name:string}>(`TRUNCATE TABLE ${table.name};`) - } - + await Promise.all(Object.values(this.#metaTables).map(table => this.#query(`TRUNCATE TABLE ${table.name};`))) } async #query(query_string:string, query_params:any[]|undefined, returnRawResult: true): Promise<{rows:row[], result:ExecuteResult}> @@ -168,7 +170,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - console.warn("QUERY: " + query_string, query_params) + // console.log("QUERY: " + query_string, query_params) if (typeof query_string != "string") {console.error("invalid query:", query_string); throw new Error("invalid query")} if (!query_string) throw new Error("empty query"); @@ -204,13 +206,17 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .build() ) if (!exists) { - await this.#createTable(definition); + await this.#createTableFromDefinition(definition); return true; } return false; } - async #createTable(definition: TableDefinition) { + + /** + * Creates a new table + */ + async #createTableFromDefinition(definition: TableDefinition) { const compositePrimaryKeyColumns = definition.columns.filter(col => col[2]?.includes("PRIMARY KEY")); if (compositePrimaryKeyColumns.length > 1) { for (const col of compositePrimaryKeyColumns) { @@ -219,9 +225,12 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } const primaryKeyDefinition = compositePrimaryKeyColumns.length > 1 ? `, PRIMARY KEY (${compositePrimaryKeyColumns.map(col => `\`${col[0]}\``).join(', ')})` : ''; + // create await this.#queryFirst(`CREATE TABLE ?? (${definition.columns.map(col => `\`${col[0]}\` ${col[1]} ${col[2]??''}` ).join(', ')}${definition.constraints?.length ? ',' + definition.constraints.join(',') : ''}${primaryKeyDefinition});`, [definition.name]) + // load column definitions + await this.#getTableColumns(definition.name); } /** @@ -298,14 +307,25 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #createTableForType(type: Datex.Type) { + + // already creating table + if (this.#tableCreationTasks.has(type)) { + return this.#tableCreationTasks.get(type); + } + + const {promise, resolve} = Promise.withResolvers(); + this.#tableCreationTasks.set(type, promise); + const columns:ColumnDefinition[] = [ [this.#pointerMysqlColumnName, this.#pointerMysqlType, 'PRIMARY KEY INVISIBLE DEFAULT "0"'] ] const constraints: ConstraintsDefinition[] = [] - this.log?.("Creating table for type " + type) - for (const [propName, propType] of Object.entries(type.template as {[key:string]:Datex.Type})) { + + // invalid prop name for now: starting/ending with _ + if (propName.startsWith("_") || propName.endsWith("_")) throw new Error("Invalid property name: " + propName + " (Property names cannot start or end with an underscore)"); + let mysqlType: mysql_data_type|undefined if (propType.base_type == Datex.Type.std.text && typeof propType.parameters?.[0] == "number") { @@ -323,7 +343,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // is a primitive type -> assume no pointer, just store as dxb inline if (propType == Type.std.Any || propType.is_primitive || propType.is_js_pseudo_primitive ) { - logger.warn("Cannot map primitive type " + propType + " to a SQL table, falling back to raw DXB") + logger.warn("Cannot map type " + propType + " to a SQL table, falling back to raw DXB") columns.push([propName, "blob"]) } else { @@ -357,7 +377,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const name = this.#typeToTableName(type); // create table - await this.#createTable({ + await this.#createTableFromDefinition({ name, columns, constraints @@ -374,6 +394,12 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .build() ) + // remember table type mapping + this.#tableTypes.set(name, type); + + // resolve promise + resolve(name); + this.#tableCreationTasks.delete(type); return name; } @@ -390,7 +416,12 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } async #getTableColumns(tableName: string) { + if (!this.#tableColumns.has(tableName)) { + if (this.#tableColumnTasks.has(tableName)) return this.#tableColumnTasks.get(tableName)!; + const {promise, resolve} = Promise.withResolvers>(); + this.#tableColumnTasks.set(tableName, promise); + const columnData = new Map() const columns = await this.#query<{COLUMN_NAME:string, COLUMN_KEY:string, DATA_TYPE:string}>( new Query() @@ -420,7 +451,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } this.#tableColumns.set(tableName, columnData) + + resolve(this.#tableColumns.get(tableName)!); + this.#tableColumnTasks.delete(tableName); } + return this.#tableColumns.get(tableName)!; } @@ -462,7 +497,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } else insertData[name] = value; } - // this.log("cols", insertData) // replace if entry already exists @@ -526,6 +560,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (type == "blob" && typeof object[colName] == "string") { object[colName] = this.#stringToBinary(object[colName] as string) } + // convert Date ot Time + else if (object[colName] instanceof Date) { + object[colName] = new Time(object[colName] as Date) + } // is an object type with a template if (foreignPtr) { @@ -577,7 +615,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { else { const pointers = new Set([pointerId]) result = (async () => { - await sleep(200); + await sleep(50); this.#templateMultiQueries.delete(table) return this.#query>( new Query() @@ -706,7 +744,19 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const rootTableName = this.#typeToTableName(valueType); // computed properties - nested select - if (options.computedProperties) { + if (options.computedProperties || options.returnRaw) { + + // add property joins for returnRaw + if (options.returnRaw) { + this.addPropertyJoins( + options.returnRaw, + builder, joins, valueType, collectedTableTypes + ) + for (const property of options.returnRaw) { + collectedIdentifiers.add(property.replaceAll(".", "__")) + } + } + const select = [...collectedIdentifiers, this.#pointerMysqlColumnName].map(identifier => { if (identifier.includes("__")) { return `${this.getTableProperty(identifier)} as ${identifier}` @@ -714,7 +764,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { else return rootTableName + '.' + identifier; }); - for (const [name, value] of Object.entries(options.computedProperties)) { + for (const [name, value] of Object.entries(options.computedProperties??{})) { if (value.type == ComputedPropertyType.GEOGRAPHIC_DISTANCE) { const computedProperty = value as ComputedProperty const {pointA, pointB} = computedProperty.data; @@ -755,7 +805,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { joins.forEach(join => builder.join(join)); const outerBuilder = new Query() - .select(`DISTINCT SQL_CALC_FOUND_ROWS ${this.#pointerMysqlColumnName} as ptrId`) + .select(options.returnRaw ? `*` :`DISTINCT SQL_CALC_FOUND_ROWS ${this.#pointerMysqlColumnName} as ptrId`) .table('__placeholder__'); this.appendBuilderConditions(outerBuilder, options, where) @@ -776,7 +826,8 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { await this.#getTableForType(type) } - const ptrIds = (await this.#query<{ptrId:string}>(query)).map(({ptrId}) => ptrId) + const queryResult = await this.#query<{ptrId:string}>(query); + const ptrIds = queryResult.map(({ptrId}) => ptrId) const limitedPtrIds = options.returnPointerIds ? // offset and limit manually after query ptrIds.slice(options.offset ?? 0, options.limit ? (options.offset ?? 0) + options.limit : undefined) : @@ -793,7 +844,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const loadStart = Date.now(); - const result = new Set((await Promise.all(limitedPtrIds.map(ptrId => Pointer.load(ptrId)))).filter(ptr => { + const result = options.returnRaw ? null : new Set((await Promise.all(limitedPtrIds.map(ptrId => Pointer.load(ptrId)))).filter(ptr => { if (ptr instanceof LazyPointer) { logger.warn("Cannot return lazy pointer from match query (" + ptr.id + ")"); return false; @@ -804,15 +855,51 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { console.log("load time", (Date.now() - loadStart) + "ms") console.log("total query time", (Date.now() - start) + "ms") + const matches = options.returnRaw ? + await Promise.all(queryResult.map(async entry => (this.mergeNestedObjects(await Promise.all(Object.entries(entry).map( + ([key, value]) => this.collapseNestedObjectEntry(key, value, rootTableName) + )))))) : + result; + if (options?.returnAdvanced) { return { - matches: result, + matches: matches, total: foundRows, ...options?.returnPointerIds ? {pointerIds: new Set(ptrIds)} : {} } as MatchResult; } else { - return result as MatchResult; + return matches as MatchResult; + } + } + + private mergeNestedObjects(insertObjects: Record[], existingObject:Record = {}): Record { + for (const insertObject of insertObjects) { + for (const [key, value] of Object.entries(insertObject)) { + if (key in existingObject && typeof value == "object" && value !== null) { + this.mergeNestedObjects([value], existingObject[key]) + } + else existingObject[key] = value; + } + } + return existingObject; + } + + private async collapseNestedObjectEntry(key: string, value: unknown, tableName: string): Promise<{[key: string]: unknown}> { + const tableDefinition = await this.#getTableColumns(tableName); + if (key.includes("__")) { + const [firstKey, ...rest] = key.split("__"); + const subTable = tableDefinition?.get(firstKey)?.foreignTable; + if (!subTable) throw new Error("No foreign table found for key " + firstKey); + return {[firstKey]: await this.collapseNestedObjectEntry(rest.join("__"), value, subTable)} + } + else { + // buffer + if (tableDefinition?.get(key)?.type == "blob" && typeof value == "string") { + value = await Runtime.decodeValue(this.#stringToBinary(value)) + } + + return {[key]: value} } } @@ -1243,7 +1330,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .map(([colName, {foreignPtr, foreignTable, type}]) => this.assignPointerProperty(object, colName, type, foreignPtr, foreignTable)) ) - this.#existingPointersCache.add(pointerId); return type.cast(object, undefined, undefined, false); } diff --git a/storage/storage.ts b/storage/storage.ts index 74c93537..2fa1cc54 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -88,6 +88,11 @@ export type MatchOptions = { * Return advanced match results (e.g. total count of matches) */ returnAdvanced?: boolean, + /** + * Provide a list of properties that should be returned as raw values. If provided, only the raw properties are returned + * and pointers are not loaded + */ + returnRaw?: string[] /** * Return pointer ids of matched items */ diff --git a/types/storage-map.ts b/types/storage-map.ts index 08606f75..4e71abe4 100644 --- a/types/storage-map.ts +++ b/types/storage-map.ts @@ -89,7 +89,6 @@ export class StorageWeakMap { if (!this.allowNonPointerObjectValues) { value = this.#pointer.proxifyChild("", value); } - console.log("SET>",storage_key, value) this.activateCacheTimeout(storage_key); await Storage.setItem(storage_key, value) return this; diff --git a/types/storage-set.ts b/types/storage-set.ts index 0a8a8673..4774d679 100644 --- a/types/storage-set.ts +++ b/types/storage-set.ts @@ -249,5 +249,4 @@ export class StorageSet extends StorageWeakSet { match(valueType:Class|Type, matchInput: MatchInput, options?: Options): Promise> { return match(this as unknown as StorageSet, valueType, matchInput, options) } - } \ No newline at end of file diff --git a/utils/match.ts b/utils/match.ts index 704a97d7..fffbea65 100644 --- a/utils/match.ts +++ b/utils/match.ts @@ -1,7 +1,7 @@ import { StorageSet } from "../types/storage_set.ts"; import { Type } from "../types/type.ts"; import type { Class } from "./global_types.ts"; -import { MatchInput, MatchResult, MatchOptions, Storage, comparatorKeys } from "../storage/storage.ts"; +import { MatchInput, MatchResult, MatchOptions, Storage } from "../storage/storage.ts"; export type { MatchInput, MatchOptions, MatchResult } from "../storage/storage.ts"; @@ -25,6 +25,7 @@ export async function match(inpu } // fallback: match by iterating over all entries + // TODO: implement full match query support for await (const input of inputSet) { // ors @@ -41,7 +42,7 @@ export async function match(inpu } } - return found; + return found as MatchResult; } function _match(value: unknown, match: unknown) { From 5fdbd5887db7d31b11a29874e320f69598e49605 Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 29 Feb 2024 19:03:01 +0100 Subject: [PATCH 33/56] make propertyInitializer optional --- types/type.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/type.ts b/types/type.ts index 35edd8a3..90deda2e 100644 --- a/types/type.ts +++ b/types/type.ts @@ -327,10 +327,10 @@ export class Type extends ExtensibleFunction { }) } - public newJSInstance(is_constructor = true, args:any[]|undefined, propertyInitializer:{[INIT_PROPS]:(instance:any)=>void}) { + public newJSInstance(is_constructor = true, args?:any[], propertyInitializer?:{[INIT_PROPS]:(instance:any)=>void}) { // create new instance - TODO 'this' as last constructor argument still required? Type.#current_constructor = this.interface_config?.class; - const instance = (this.interface_config?.class ? Reflect.construct(Type.#current_constructor, is_constructor?[...args]:[propertyInitializer]) : {[DX_TYPE]: this}); + const instance = (this.interface_config?.class ? Reflect.construct(Type.#current_constructor, is_constructor?[...args]:(propertyInitializer ? [propertyInitializer] : [])) : {[DX_TYPE]: this}); Type.#current_constructor = null; return instance; } From 2f2958e11a7a8c4875551d807f37d676b1c9b542 Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 29 Feb 2024 20:37:16 +0100 Subject: [PATCH 34/56] fix major bug regarding storage location dirty state leading to pointer loss (unrelated) --- storage/storage.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/storage/storage.ts b/storage/storage.ts index 2fa1cc54..101c27f2 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -540,14 +540,26 @@ export class Storage { return Number(localStorage.getItem(this.meta_prefix+'__saved__' + location.name) ?? 0); } - static #dirty_locations = new Set() - // handle dirty states for async storage operations: + static #dirty_locations = new Map() // called when a full backup to this storage location was made public static setDirty(location:StorageLocation, dirty = true) { - if (dirty) this.#dirty_locations.add(location); - else this.#dirty_locations.delete(location); + // update counter + if (dirty) { + const currentCount = this.#dirty_locations.get(location)??0; + this.#dirty_locations.set(location, currentCount + 1); + } + else { + if (!this.#dirty_locations.has(location)) logger.warn("Invalid dirty state reset for location '"+location.name + "', dirty state was not set"); + else { + const newCount = this.#dirty_locations.get(location)! - 1; + if (newCount <= 0) { + this.#dirty_locations.delete(location); + } + else this.#dirty_locations.set(location, newCount); + } + } } static #dirty = false; @@ -561,7 +573,7 @@ export class Storage { */ private static saveDirtyState(){ if (this.#exit_without_save) return; // currently exiting - for (const location of this.#dirty_locations) { + for (const [location] of this.#dirty_locations) { localStorage.setItem(this.meta_prefix+'__dirty__' + location.name, new Date().getTime().toString()); } } @@ -761,10 +773,10 @@ export class Storage { * @param location storage location */ private static async saveDependencyPointersAsync(dependencies: Set, listen_for_changes = true, location: AsyncStorageLocation) { - for (const ptr of dependencies) { + await Promise.all([...dependencies].map(async ptr=>{ // add if not yet in storage if (!await location.hasPointer(ptr.id)) await this.setPointer(ptr, listen_for_changes, location) - } + })); } @@ -859,7 +871,7 @@ export class Storage { if (this.#primary_location != undefined && this.isInDirtyState(this.#primary_location) && this.#trusted_location != undefined && this.#trusted_location!=this.#primary_location) { await this.copyStorage(this.#trusted_location, this.#primary_location) logger.warn `restored dirty state of ${this.#primary_location.name} from trusted location ${this.#trusted_location.name}` - this.setDirty(this.#primary_location, false) // remove from dirty set + if (this.#dirty_locations.has(this.#primary_location)) this.setDirty(this.#primary_location, false) // remove from dirty set this.clearDirtyState(this.#primary_location) // remove from localstorage this.#dirty = false; // primary location is now trusted, update From ac4fd1e93a87ce69199c4f9be9fd1bbe1ccacb9e Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 29 Feb 2024 22:43:39 +0100 Subject: [PATCH 35/56] started work on serializable js transforms (not yet finished) --- compiler/compiler.ts | 32 ++++++++++++++++++++++++++------ runtime/pointers.ts | 5 +++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/compiler/compiler.ts b/compiler/compiler.ts index f61a7c70..3f6fae9d 100644 --- a/compiler/compiler.ts +++ b/compiler/compiler.ts @@ -13,7 +13,7 @@ import { Logger } from "../utils/logger.ts"; const logger = new Logger("datex compiler"); -import { ReadableStream, Runtime, StaticScope} from "../runtime/runtime.ts"; +import { ReadableStream, Runtime } from "../runtime/runtime.ts"; import { Endpoint, IdEndpoint, Target, WildcardTarget, Institution, Person, BROADCAST, target_clause, endpoints, LOCAL_ENDPOINT } from "../types/addressing.ts"; import { Pointer, PointerProperty, Ref } from "../runtime/pointers.ts"; import { CompilerError, RuntimeError, Error as DatexError, ValueError } from "../types/errors.ts"; @@ -2860,12 +2860,32 @@ export class Compiler { // handle pointers with transform (always ...) // only if not ignore_first_collapse or, if ignore_first_collapse and keep_first_transform is enabled - if (!SCOPE.options.no_create_pointers && value instanceof Pointer && value.transform_scope && (value.force_local_transform || !skip_first_collapse || SCOPE.options.keep_first_transform)) { - SCOPE.options._first_insert_done = true; // set to true before next insert + if (!SCOPE.options.no_create_pointers && value instanceof Pointer && (value.force_local_transform || !skip_first_collapse || SCOPE.options.keep_first_transform)) { + + if (value.transform_scope) { + SCOPE.options._first_insert_done = true; // set to true before next insert + Compiler.builder.insert_transform_scope(SCOPE, value.transform_scope); + return; + } - Compiler.builder.insert_transform_scope(SCOPE, value.transform_scope); - - return; + else if (value.smart_transform_method) { + console.warn("DATEX serialization of JS transforms is not yet supported: ", value.smart_transform_method.toString()) + // TODO: + // const isTransferableFn = value.smart_transform_method instanceof JSTransferableFunction; + // const transferableFn = isTransferableFn ? value.smart_transform_method as unknown as JSTransferableFunction : JSTransferableFunction.create(value.smart_transform_method); + // if (!isTransferableFn) transferableFn.source = `() => always(${transferableFn.source})`; + + // value.smart_transform_method = transferableFn; + // Compiler.builder.handleRequiredBufferSize(SCOPE.b_index+2, SCOPE); + // SCOPE.uint8[SCOPE.b_index++] = BinaryCode.SUBSCOPE_START; + // SCOPE.uint8[SCOPE.b_index++] = BinaryCode.CREATE_POINTER; + // Compiler.builder.insert(transferableFn, SCOPE, is_root, parents, unassigned_children); + // Compiler.builder.handleRequiredBufferSize(SCOPE.b_index+2, SCOPE); + // SCOPE.uint8[SCOPE.b_index++] = BinaryCode.SUBSCOPE_END; + // SCOPE.uint8[SCOPE.b_index++] = BinaryCode.VOID; + // return; + } + } // indirect reference pointer diff --git a/runtime/pointers.ts b/runtime/pointers.ts index 76c6c9f1..cb80a009 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -2766,6 +2766,10 @@ export class Pointer extends Ref { #transform_scope?:Scope; get transform_scope() {return this.#transform_scope} + #smart_transform_method?: (...args:any[])=>any + get smart_transform_method() {return this.#smart_transform_method} + set smart_transform_method(method: (...args:any[])=>any) {this.#smart_transform_method = method} + #force_transform = false; // if true, the pointer transform function is always sent via DATEX set force_local_transform(force_transform: boolean) {this.#force_transform = force_transform} get force_local_transform() {return this.#force_transform} @@ -2846,6 +2850,7 @@ export class Pointer extends Ref { protected smartTransform(transform:SmartTransformFunction, persistent_datex_transform?:string, forceLive = false, ignoreReturnValue = false, options?:SmartTransformOptions): Pointer { if (persistent_datex_transform) this.setDatexTransform(persistent_datex_transform) // TODO: only workaround + this.#smart_transform_method = transform; const state: TransformState = { isLive: false, From ab074ad95c8a8e45a354433778475891e8e424b3 Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 00:21:14 +0100 Subject: [PATCH 36/56] sql db fixes --- storage/storage-locations/sql-db.ts | 39 ++++++++++++++++++----------- storage/storage.ts | 4 +-- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index ff843d00..d8c40b4a 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -493,7 +493,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } // is raw dxb value (exception for blob <->A rrayBuffer, TODO: better solution, can lead to issues) else if (type == "blob" && !(value instanceof ArrayBuffer || value instanceof TypedArray)) { - insertData[name] = Compiler.encodeValue(value, dependencies, true, false, true); + insertData[name] = Compiler.encodeValue(value, dependencies, true, false, false); } else insertData[name] = value; } @@ -511,7 +511,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { /** * Update a pointer in the database, pointer type must be templated */ - async #updatePointer(pointer: Datex.Pointer, keys:string[]) { + async #updatePointer(pointer: Datex.Pointer, keys:string[], dependencies?: Set) { const table = await this.#getTableForType(pointer.type); if (!table) throw new Error("Cannot store pointer of type " + pointer.type + " in a custom table") const columns = await this.#getTableColumns(table); @@ -525,7 +525,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { ( (column?.type == "blob" && !(pointer.val[key] instanceof ArrayBuffer || pointer.val[key] instanceof TypedArray)) ? // raw dxb value - Compiler.encodeValue(pointer.val[key], new Set(), true, false, true) : + Compiler.encodeValue(pointer.val[key], dependencies, true, false, false) : // normal value pointer.val[key] ) @@ -560,11 +560,16 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (type == "blob" && typeof object[colName] == "string") { object[colName] = this.#stringToBinary(object[colName] as string) } - // convert Date ot Time + // convert Date to Time else if (object[colName] instanceof Date) { object[colName] = new Time(object[colName] as Date) } + // convert to boolean + else if (typeof object[colName] == "number" && (type == "tinyint" || type == "boolean")) { + object[colName] = Boolean(object[colName]) + } + // is an object type with a template if (foreignPtr) { if (typeof object[colName] == "string") { @@ -576,23 +581,22 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } // is blob, assume it is a DXB value else if (type == "blob") { - object[colName] = `\u0001${foreignPointerPlaceholders.length}` try { // TODO: fix decompiling - foreignPointerPlaceholders.push(MessageLogger.decompile(object[colName] as ArrayBuffer, false, false, false)||"void") + foreignPointerPlaceholders.push(Storage.removeTrailingSemicolon(MessageLogger.decompile(object[colName] as ArrayBuffer, false, false, false)||"'error'")) } catch (e) { console.error("error decompiling", object[colName], e) foreignPointerPlaceholders.push("'error'") } - + object[colName] = `\u0001${foreignPointerPlaceholders.length-1}` } } // const foreignPointerPlaceholders = await Promise.all(foreignPointerPlaceholderPromises) const objectString = Datex.Runtime.valueToDatexStringExperimental(object, false, false) - .replace(/"\u0001(\d+)"/g, (_, index) => foreignPointerPlaceholders[parseInt(index)]||"error: no placeholder") + .replace(/"\u0001(\d+)"/g, (_, index) => foreignPointerPlaceholders[parseInt(index)]||"'error: no placeholder'") return `${type.toString()} ${objectString}` } @@ -615,7 +619,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { else { const pointers = new Set([pointerId]) result = (async () => { - await sleep(50); + await sleep(30); this.#templateMultiQueries.delete(table) return this.#query>( new Query() @@ -628,8 +632,10 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { this.#templateMultiQueries.set(table, {pointers, result}) } - return (await result). - find(obj => obj[this.#pointerMysqlColumnName] == pointerId); + const res = (await result) + .find(obj => obj[this.#pointerMysqlColumnName] == pointerId) + if (res) delete res[this.#pointerMysqlColumnName]; + return res; } async #getTemplatedPointerValueDXB(pointerId: string, table?: string) { @@ -695,7 +701,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { data.value_pointer = valPtr.id dependencies.add(valPtr); } - else data.value_dxb = Compiler.encodeValue(val, dependencies, true, false, true) + else data.value_dxb = this.#binaryToString(Compiler.encodeValue(val, dependencies, true, false, false)) entries.push(data) } builder.insert(entries); @@ -1248,7 +1254,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } await Promise.all(promises) - await this.#updatePointer(pointer, [partialUpdateKey]) + await this.#updatePointer(pointer, [partialUpdateKey], dependencies) return dependencies; } } @@ -1302,7 +1308,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { if (value_text != undefined) result.add(value_text) else if (value_integer != undefined) result.add(BigInt(value_integer)) else if (value_decimal != undefined) result.add(value_decimal) - else if (value_boolean != undefined) result.add(value_boolean) + else if (value_boolean != undefined) result.add(Boolean(value_boolean)) else if (value_time != undefined) result.add(value_time) else if (value_pointer != undefined) result.add(await Pointer.load(value_pointer)) else if (value_dxb != undefined) result.add(await Runtime.decodeValue(this.#stringToBinary(value_dxb))) @@ -1345,6 +1351,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { else if (object[colName] instanceof Date) { object[colName] = new Time(object[colName] as Date) } + + // convert to boolean + else if (typeof object[colName] == "number" && (type == "tinyint" || type == "boolean")) { + object[colName] = Boolean(object[colName]) + } // is an object type with a template if (foreignPtr) { diff --git a/storage/storage.ts b/storage/storage.ts index 101c27f2..ab7b416c 100644 --- a/storage/storage.ts +++ b/storage/storage.ts @@ -1539,9 +1539,9 @@ export class Storage { } - private static removeTrailingSemicolon(str:string) { + public static removeTrailingSemicolon(str:string) { // replace ; and reset sequences with nothing - return str.replace(/;\x1b\[0m$/g, "") + return str.replace(/;(\x1b\[0m)?$/g, "") } public static async getSnapshot(options: StorageSnapshotOptions = {internalItems: false, expandStorageMapsAndSets: true}) { From 133e2829a3311461ab2b4699f915d5890732a364 Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 00:43:18 +0100 Subject: [PATCH 37/56] improve sql parallel queries --- storage/storage-locations/sql-db.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index d8c40b4a..bddcff1a 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -97,6 +97,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { #existingPointersCache = new Set() #tableCreationTasks = new Map>() #tableColumnTasks = new Map>>() + #tableLoadingTasks = new Map>() // remember tables for pointers that still need to be loaded #pointerTables = new Map() @@ -170,7 +171,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - // console.log("QUERY: " + query_string, query_params) + console.log("QUERY: " + query_string, query_params) if (typeof query_string != "string") {console.error("invalid query:", query_string); throw new Error("invalid query")} if (!query_string) throw new Error("empty query"); @@ -226,7 +227,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const primaryKeyDefinition = compositePrimaryKeyColumns.length > 1 ? `, PRIMARY KEY (${compositePrimaryKeyColumns.map(col => `\`${col[0]}\``).join(', ')})` : ''; // create - await this.#queryFirst(`CREATE TABLE ?? (${definition.columns.map(col => + await this.#queryFirst(`CREATE TABLE IF NOT EXISTS ?? (${definition.columns.map(col => `\`${col[0]}\` ${col[1]} ${col[2]??''}` ).join(', ')}${definition.constraints?.length ? ',' + definition.constraints.join(',') : ''}${primaryKeyDefinition});`, [definition.name]) // load column definitions @@ -242,6 +243,14 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // type does not have a template, use raw pointer table if (!type.template) return null + // already creating table + if (this.#tableLoadingTasks.has(type)) { + return this.#tableLoadingTasks.get(type); + } + + const {promise, resolve} = Promise.withResolvers(); + this.#tableLoadingTasks.set(type, promise); + // already has a table const tableName = this.#typeToTableName(type); if (this.#tableTypes.has(tableName)) return tableName; @@ -254,10 +263,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { .where(Where.eq("type", this.#typeToString(type))) .build() ))?.table_name; - if (!existingTable) { - return this.#createTableForType(type) - } - else return existingTable + + const table = existingTable ?? await this.#createTableForType(type); + resolve(table); + this.#tableLoadingTasks.delete(type); + return table; } async #getTypeForTable(table: string) { From df1bcbcce2c584469d22395c4cd0f213c7f0000b Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 00:48:58 +0100 Subject: [PATCH 38/56] sql fixes --- storage/storage-locations/sql-db.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index bddcff1a..ea406db4 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -243,6 +243,11 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { // type does not have a template, use raw pointer table if (!type.template) return null + // already has a table + const tableName = this.#typeToTableName(type); + if (this.#tableTypes.has(tableName)) return tableName; + + // already creating table if (this.#tableLoadingTasks.has(type)) { return this.#tableLoadingTasks.get(type); @@ -251,11 +256,6 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { const {promise, resolve} = Promise.withResolvers(); this.#tableLoadingTasks.set(type, promise); - // already has a table - const tableName = this.#typeToTableName(type); - if (this.#tableTypes.has(tableName)) return tableName; - - const existingTable = (await this.#queryFirst<{table_name: string}|undefined>( new Query() .table(this.#metaTables.typeMapping.name) From 2585195bcae4dc5a988d5efc27e1c2a84c9305ff Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 00:50:32 +0100 Subject: [PATCH 39/56] remove debug log --- storage/storage-locations/sql-db.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage/storage-locations/sql-db.ts b/storage/storage-locations/sql-db.ts index ea406db4..1fd233d8 100644 --- a/storage/storage-locations/sql-db.ts +++ b/storage/storage-locations/sql-db.ts @@ -171,7 +171,7 @@ export class SQLDBStorageLocation extends AsyncStorageLocation { } } - console.log("QUERY: " + query_string, query_params) + // console.log("QUERY: " + query_string, query_params) if (typeof query_string != "string") {console.error("invalid query:", query_string); throw new Error("invalid query")} if (!query_string) throw new Error("empty query"); From ef34712257beb0b5556087f8bf47b507143f27b9 Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 14:07:31 +0100 Subject: [PATCH 40/56] add DATEX js:RegExp support --- runtime/runtime.ts | 17 ++++++++++++++++- types/type.ts | 13 ++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/runtime/runtime.ts b/runtime/runtime.ts index 3fbe736a..c7b6b501 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -2267,7 +2267,7 @@ export class Runtime { let new_value:any = UNKNOWN_TYPE; // only handle std namespace / js:Object / js:Symbol - if (type.namespace == "std" || type == Type.js.NativeObject || type == Type.js.Symbol) { + if (type.namespace == "std" || type == Type.js.NativeObject || type == Type.js.Symbol || type == Type.js.RegExp) { const uncollapsed_old_value = old_value if (old_value instanceof Pointer) old_value = old_value.val; @@ -2354,6 +2354,18 @@ export class Runtime { else new_value = INVALID; break; } + case Type.js.RegExp: { + if (typeof old_value == "string") new_value = new RegExp(old_value); + else if (old_value instanceof Tuple) { + const array = old_value.toArray() as [string, string?]; + new_value = new RegExp(...array); + } + else if (old_value instanceof Array) { + new_value = new RegExp(...old_value as [string, string?]); + } + else new_value = INVALID; + break; + } case Type.std.Tuple: { if (old_value === VOID) new_value = new Tuple().seal(); else if (old_value instanceof Array){ @@ -2690,6 +2702,9 @@ export class Runtime { // symbol if (typeof value == "symbol") return value.toString().slice(7,-1) || undefined + // regex + if (value instanceof RegExp) return value.flags ? new Tuple([value.source, value.flags]) : value.source; + // weakref if (value instanceof WeakRef) { const deref = value.deref(); diff --git a/types/type.ts b/types/type.ts index 90deda2e..dffb73dd 100644 --- a/types/type.ts +++ b/types/type.ts @@ -42,7 +42,7 @@ export class Type extends ExtensibleFunction { // should be serialized, but is not a complex type (per default, only complex types are serialized) static serializable_not_complex_types = ["buffer"] // values that are represented js objects but have a single instance per value, handle like normal js primitives - static pseudo_js_primitives = ["Type", "endpoint", "target", "url"] + static pseudo_js_primitives = ["Type", "endpoint", "target", "url", "RegExp"] public static types = new Map(); // type name -> type @@ -392,7 +392,7 @@ export class Type extends ExtensibleFunction { this.is_primitive = namespace=="std" && Type.primitive_types.includes(this.name); this.is_complex = namespace!="std" || !Type.fundamental_types.includes(this.name); - this.is_js_pseudo_primitive = namespace=="std" && Type.pseudo_js_primitives.includes(this.name); + this.is_js_pseudo_primitive = (namespace=="std"||namespace=="js") && Type.pseudo_js_primitives.includes(this.name); this.has_compact_rep = namespace=="std" && (this.is_primitive || Type.compact_rep_types.includes(this.name)); this.serializable_not_complex = Type.serializable_not_complex_types.includes(this.name); @@ -815,7 +815,8 @@ export class Type extends ExtensibleFunction { if (typeof value == "bigint") return >Type.std.integer; if (typeof value == "number") return >Type.std.decimal; if (typeof value == "boolean") return >Type.std.boolean; - if (typeof value == "symbol") return Type.js.Symbol; + if (typeof value == "symbol") return Type.js.Symbol as unknown as Type; + if (value instanceof RegExp) return Type.js.RegExp as unknown as Type; if (value instanceof ArrayBuffer || value instanceof TypedArray) return >Type.std.buffer; if (value instanceof Tuple) return >Type.std.Tuple; @@ -889,6 +890,7 @@ export class Type extends ExtensibleFunction { if (_forClass == Number || Number.isPrototypeOf(_forClass)) return >Type.std.decimal; if (_forClass == globalThis.Boolean || globalThis.Boolean.isPrototypeOf(_forClass)) return >Type.std.boolean; if (_forClass == Symbol || Symbol.isPrototypeOf(_forClass)) return >Type.js.Symbol; + if (_forClass == RegExp || RegExp.isPrototypeOf(_forClass)) return Type.js.RegExp as unknown as Type; if (_forClass == WeakRef || WeakRef.isPrototypeOf(_forClass)) return >Type.std.WeakRef; if (_forClass == ArrayBuffer || TypedArray.isPrototypeOf(_forClass)) return >Type.std.buffer; @@ -949,7 +951,8 @@ export class Type extends ExtensibleFunction { static js = { NativeObject: Type.get("js:Object"), // special object type for non-plain objects (objects with prototype) - no automatic children pointer initialization TransferableFunction: Type.get("js:Function"), - Symbol: Type.get("js:Symbol") + Symbol: Type.get("js:Symbol"), + RegExp: Type.get("js:RegExp") } /** @@ -1026,7 +1029,7 @@ export class Type extends ExtensibleFunction { Assertion: Type.get("std:Assertion"), Iterator: Type.get>("std:Iterator"), - MatchCondition: Type.get("std:MatchCondition"), + MatchCondition: Type.get>("std:MatchCondition"), StorageMap: Type.get>("std:StorageMap"), StorageWeakMap: Type.get>("std:StorageWeakMap"), From b5a411f79aeae3dc2087863bfdfaba61abee7d9e Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 14:10:23 +0100 Subject: [PATCH 41/56] add docs table for support of builtin js types --- docs/manual/11 Types.md | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/manual/11 Types.md b/docs/manual/11 Types.md index 3f0c9753..e095cf83 100644 --- a/docs/manual/11 Types.md +++ b/docs/manual/11 Types.md @@ -27,6 +27,43 @@ Datex.Type.std.boolean === boolean Datex.Type.std.Any === any ``` +## Supported built-in JS and Web types +| **JS Type** | **Support** | **DATEX Type** | **Synchronizable** | **Limitations** | +|--------------------------------|-------------|----------------|--------------------|-------------------------------------------------------------------------------------------| +| **string** | Full | std:text | 1) | 3) | +| **number** | Full | std:decimal | 1) | 3) | +| **bigint** | Full | std:integer | 1) | 3) | +| **boolean** | Full | std:boolean | 1) | 3) | +| **null** | Full | std:null | 1) | 3) | +| **undefined** | Full | std:void | 1) | 3) | +| **symbol** | Partial | js:Symbol | 1) | Registered and well-known symbols are not yet supported | +| **Object (without prototype)** | Full | std:Object | Yes | Objects with prototypes other than `Object.prototype` or `null` are mapped to `js:Object` | +| **Object** | Sufficient | js:Object | Yes | No synchronisation for nested objects per default | +| **Array** | Full | std:Array | Yes | - | +| **Set** | Full | std:Set | Yes | - | +| **Map** | Full | std:Map | Yes | - | +| **WeakSet** | None | - | - | Cannot be implemented because `WeakSet` internals are not accessible. Alternative: `StorageWeakSet` | +| **WeakMap** | None | - | - | Cannot be implemented because `WeakMap` internals are not accessible. Alternative: `StorageWeakMap` | +| **Function** | Sufficient | std:Function | No (Immutable) | Functions always return a Promise, even if they are synchronous | +| **AsyncFunction** | Sufficient | std:Function | No (Immutable) | - | +| **GeneratorFunction** | None | - | - | - | +| **ArrayBuffer** | Partial | std:buffer | No | ArrayBuffer mutations are currently not synchronized | +| **URL** | Partial | std:url | No | URL mutations are currently not synchronized | +| **Date** | Partial | std:time | No | `Date` objects are currently asymetrically mapped to DATEX `Time` objects | +| **RegExp** | Partial | js:RegExp | No (Immutable) | RegExp values wrapped in a Ref are currently not synchronized | +| **WeakRef** | Full | std:WeakRef | No (Immutable) | - | +| **Error** | Partial | std:Error | No | Error subclasses are not correctly mapped | +| **HTMLElement** | Partial 2) | std:html | No | HTML element mutations are currently not synchronized | +| **SVGElement** | Partial 2) | std:svg | No | SVG element mutations are currently not synchronized | +| **MathMLElement** | Partial 2) | std:mathml | No | MathML element mutations are currently not synchronized | +| **Document** | Partial 2) | std:htmldocument | No | Document mutations are currently not synchronized | +| **DocumentFragment** | Partial 2) | std:htmlfragment | No | DocumentFragment mutations are currently not synchronized | + + +1) Primitive JS values are immutable and cannot be synchronized on their own, but when wrapped in a Ref. +2) [UIX-DOM](https://github.com/unyt-org/uix-dom) required +3) The corresponding object values of primitive values (e.g. `new Number()` for `number`) are not supported + ## Special JS types Most builtin JavaScript types, like Map, Set or Array have equivalent types in the DATEX std library. @@ -141,6 +178,6 @@ A struct definition accepts strings a keys and `Datex.Type`s, JavaScript classes or other struct definitions as values. -## Mapping JS classes to DATEX types +## Mapping your own JS classes to DATEX types Check out the chapter [11 Classes](./11%20Classes.md) for more information. \ No newline at end of file From c9f2775ce34f11754b82309e3c072ddb01e2c08a Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 14:12:55 +0100 Subject: [PATCH 42/56] update table formatting --- docs/manual/11 Types.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/manual/11 Types.md b/docs/manual/11 Types.md index e095cf83..31b105d1 100644 --- a/docs/manual/11 Types.md +++ b/docs/manual/11 Types.md @@ -29,13 +29,13 @@ Datex.Type.std.Any === any ## Supported built-in JS and Web types | **JS Type** | **Support** | **DATEX Type** | **Synchronizable** | **Limitations** | -|--------------------------------|-------------|----------------|--------------------|-------------------------------------------------------------------------------------------| +|:-------------------------------|:------------|:---------------|:-------------------|:------------------------------------------------------------------------------------------| | **string** | Full | std:text | 1) | 3) | | **number** | Full | std:decimal | 1) | 3) | | **bigint** | Full | std:integer | 1) | 3) | | **boolean** | Full | std:boolean | 1) | 3) | -| **null** | Full | std:null | 1) | 3) | -| **undefined** | Full | std:void | 1) | 3) | +| **null** | Full | std:null | 1) | - | +| **undefined** | Full | std:void | 1) | - | | **symbol** | Partial | js:Symbol | 1) | Registered and well-known symbols are not yet supported | | **Object (without prototype)** | Full | std:Object | Yes | Objects with prototypes other than `Object.prototype` or `null` are mapped to `js:Object` | | **Object** | Sufficient | js:Object | Yes | No synchronisation for nested objects per default | From 25efa0f979182c9737e169804a57724a32fbb07e Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 14:15:23 +0100 Subject: [PATCH 43/56] update table formatting --- docs/manual/11 Types.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/manual/11 Types.md b/docs/manual/11 Types.md index 31b105d1..6d92a1de 100644 --- a/docs/manual/11 Types.md +++ b/docs/manual/11 Types.md @@ -4,7 +4,7 @@ The DATEX Runtime comes with its own type system which can be mapped to JavaScri DATEX types can be access via `Datex.Type`. ## Std types -The `Datex.Type.std` namespace contains all the builtin (*std*) DATEX types, e.g.: +The `Datex.Type.std` namespace contains all the builtin (*std*) DATEX types that can be accessed as runtime values, e.g.: ```ts // primitive types Datex.Type.std.text @@ -30,13 +30,13 @@ Datex.Type.std.Any === any ## Supported built-in JS and Web types | **JS Type** | **Support** | **DATEX Type** | **Synchronizable** | **Limitations** | |:-------------------------------|:------------|:---------------|:-------------------|:------------------------------------------------------------------------------------------| -| **string** | Full | std:text | 1) | 3) | -| **number** | Full | std:decimal | 1) | 3) | -| **bigint** | Full | std:integer | 1) | 3) | -| **boolean** | Full | std:boolean | 1) | 3) | -| **null** | Full | std:null | 1) | - | -| **undefined** | Full | std:void | 1) | - | -| **symbol** | Partial | js:Symbol | 1) | Registered and well-known symbols are not yet supported | +| **string** | Full | std:text | Yes 1) | 3) | +| **number** | Full | std:decimal | Yes 1) | 3) | +| **bigint** | Full | std:integer | Yes 1) | 3) | +| **boolean** | Full | std:boolean | Yes 1) | 3) | +| **null** | Full | std:null | Yes 1) | - | +| **undefined** | Full | std:void | Yes 1) | - | +| **symbol** | Partial | js:Symbol | Yes 1) | Registered and well-known symbols are not yet supported | | **Object (without prototype)** | Full | std:Object | Yes | Objects with prototypes other than `Object.prototype` or `null` are mapped to `js:Object` | | **Object** | Sufficient | js:Object | Yes | No synchronisation for nested objects per default | | **Array** | Full | std:Array | Yes | - | From 985f39366a54df1fe044d1e9089614d7c7798a2d Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 14:17:54 +0100 Subject: [PATCH 44/56] update table formatting --- docs/manual/11 Types.md | 70 ++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/manual/11 Types.md b/docs/manual/11 Types.md index 6d92a1de..622051a7 100644 --- a/docs/manual/11 Types.md +++ b/docs/manual/11 Types.md @@ -28,41 +28,41 @@ Datex.Type.std.Any === any ``` ## Supported built-in JS and Web types -| **JS Type** | **Support** | **DATEX Type** | **Synchronizable** | **Limitations** | -|:-------------------------------|:------------|:---------------|:-------------------|:------------------------------------------------------------------------------------------| -| **string** | Full | std:text | Yes 1) | 3) | -| **number** | Full | std:decimal | Yes 1) | 3) | -| **bigint** | Full | std:integer | Yes 1) | 3) | -| **boolean** | Full | std:boolean | Yes 1) | 3) | -| **null** | Full | std:null | Yes 1) | - | -| **undefined** | Full | std:void | Yes 1) | - | -| **symbol** | Partial | js:Symbol | Yes 1) | Registered and well-known symbols are not yet supported | -| **Object (without prototype)** | Full | std:Object | Yes | Objects with prototypes other than `Object.prototype` or `null` are mapped to `js:Object` | -| **Object** | Sufficient | js:Object | Yes | No synchronisation for nested objects per default | -| **Array** | Full | std:Array | Yes | - | -| **Set** | Full | std:Set | Yes | - | -| **Map** | Full | std:Map | Yes | - | -| **WeakSet** | None | - | - | Cannot be implemented because `WeakSet` internals are not accessible. Alternative: `StorageWeakSet` | -| **WeakMap** | None | - | - | Cannot be implemented because `WeakMap` internals are not accessible. Alternative: `StorageWeakMap` | -| **Function** | Sufficient | std:Function | No (Immutable) | Functions always return a Promise, even if they are synchronous | -| **AsyncFunction** | Sufficient | std:Function | No (Immutable) | - | -| **GeneratorFunction** | None | - | - | - | -| **ArrayBuffer** | Partial | std:buffer | No | ArrayBuffer mutations are currently not synchronized | -| **URL** | Partial | std:url | No | URL mutations are currently not synchronized | -| **Date** | Partial | std:time | No | `Date` objects are currently asymetrically mapped to DATEX `Time` objects | -| **RegExp** | Partial | js:RegExp | No (Immutable) | RegExp values wrapped in a Ref are currently not synchronized | -| **WeakRef** | Full | std:WeakRef | No (Immutable) | - | -| **Error** | Partial | std:Error | No | Error subclasses are not correctly mapped | -| **HTMLElement** | Partial 2) | std:html | No | HTML element mutations are currently not synchronized | -| **SVGElement** | Partial 2) | std:svg | No | SVG element mutations are currently not synchronized | -| **MathMLElement** | Partial 2) | std:mathml | No | MathML element mutations are currently not synchronized | -| **Document** | Partial 2) | std:htmldocument | No | Document mutations are currently not synchronized | -| **DocumentFragment** | Partial 2) | std:htmlfragment | No | DocumentFragment mutations are currently not synchronized | - - -1) Primitive JS values are immutable and cannot be synchronized on their own, but when wrapped in a Ref. -2) [UIX-DOM](https://github.com/unyt-org/uix-dom) required -3) The corresponding object values of primitive values (e.g. `new Number()` for `number`) are not supported +| **JS Type** | **Support** | **DATEX Type** | **Synchronizable** | **Limitations** | +|:-------------------------------|:----------------------|:---------------|:-------------------|:------------------------------------------------------------------------------------------| +| **string** | Full | std:text | Yes 1) | 3) | +| **number** | Full | std:decimal | Yes 1) | 3) | +| **bigint** | Full | std:integer | Yes 1) | 3) | +| **boolean** | Full | std:boolean | Yes 1) | 3) | +| **null** | Full | std:null | Yes 1) | - | +| **undefined** | Full | std:void | Yes 1) | - | +| **symbol** | Partial | js:Symbol | Yes 1) | Registered and well-known symbols are not yet supported | +| **Object (without prototype)** | Full | std:Object | Yes | Objects with prototypes other than `Object.prototype` or `null` are mapped to `js:Object` | +| **Object** | Sufficient | js:Object | Yes | No synchronisation for nested objects per default | +| **Array** | Full | std:Array | Yes | - | +| **Set** | Full | std:Set | Yes | - | +| **Map** | Full | std:Map | Yes | - | +| **WeakSet** | None | - | - | Cannot be implemented because `WeakSet` internals are not accessible. Alternative: `StorageWeakSet` | +| **WeakMap** | None | - | - | Cannot be implemented because `WeakMap` internals are not accessible. Alternative: `StorageWeakMap` | +| **Function** | Sufficient | std:Function | No (Immutable) | Functions always return a Promise, even if they are synchronous | +| **AsyncFunction** | Sufficient | std:Function | No (Immutable) | - | +| **GeneratorFunction** | None | - | - | - | +| **ArrayBuffer** | Partial | std:buffer | No | ArrayBuffer mutations are currently not synchronized | +| **URL** | Partial | std:url | No | URL mutations are currently not synchronized | +| **Date** | Partial | std:time | No | `Date` objects are currently asymetrically mapped to DATEX `Time` objects | +| **RegExp** | Partial | js:RegExp | No (Immutable) | RegExp values wrapped in a Ref are currently not synchronized | +| **WeakRef** | Full | std:WeakRef | No (Immutable) | - | +| **Error** | Partial | std:Error | No | Error subclasses are not correctly mapped | +| **HTMLElement** | Partial 2) | std:html | No | HTML element mutations are currently not synchronized | +| **SVGElement** | Partial 2) | std:svg | No | SVG element mutations are currently not synchronized | +| **MathMLElement** | Partial 2) | std:mathml | No | MathML element mutations are currently not synchronized | +| **Document** | Partial 2) | std:htmldocument | No | Document mutations are currently not synchronized | +| **DocumentFragment** | Partial 2) | std:htmlfragment | No | DocumentFragment mutations are currently not synchronized | + + +1) Primitive JS values are immutable and cannot be synchronized on their own, but when wrapped in a Ref. +2) [UIX-DOM](https://github.com/unyt-org/uix-dom) required +3) The corresponding object values of primitive values (e.g. `new Number()` for `number`) are not supported ## Special JS types From 6409b6895e7772f10860be67f267a2dfb2b4af0b Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 14:18:18 +0100 Subject: [PATCH 45/56] update table formatting --- docs/manual/11 Types.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/manual/11 Types.md b/docs/manual/11 Types.md index 622051a7..de90ec53 100644 --- a/docs/manual/11 Types.md +++ b/docs/manual/11 Types.md @@ -60,9 +60,9 @@ Datex.Type.std.Any === any | **DocumentFragment** | Partial 2) | std:htmlfragment | No | DocumentFragment mutations are currently not synchronized | -1) Primitive JS values are immutable and cannot be synchronized on their own, but when wrapped in a Ref. -2) [UIX-DOM](https://github.com/unyt-org/uix-dom) required -3) The corresponding object values of primitive values (e.g. `new Number()` for `number`) are not supported +1) Primitive JS values are immutable and cannot be synchronized on their own, but when wrapped in a Ref.
    +2) [UIX-DOM](https://github.com/unyt-org/uix-dom) required
    +3) The corresponding object values of primitive values (e.g. `new Number()` for `number`) are not supported
    ## Special JS types From d5fceddcb171e361da450b7207bdb5db436740c0 Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 20:57:55 +0100 Subject: [PATCH 46/56] minor fixes --- runtime/pointers.ts | 6 +++--- runtime/runtime.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/runtime/pointers.ts b/runtime/pointers.ts index cb80a009..60bbd88a 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -791,18 +791,18 @@ type _Proxy$ = _Proxy$Function & T extends Array ? // array { - [key: number]: RefLikeOut, + [key: number]: RefLike, map(callbackfn: (value: MaybeObjectRef, index: number, array: V[]) => U, thisArg?: any): Pointer } : ( T extends Map ? { - get(key: K): RefLikeOut + get(key: K): RefLike } // normal object - : {readonly [K in keyof T]: RefLikeOut} // always map properties to pointer property references + : {readonly [K in keyof T]: RefLike} // always map properties to pointer property references ) type _PropertyProxy$ = _Proxy$Function & diff --git a/runtime/runtime.ts b/runtime/runtime.ts index c7b6b501..0fe1995d 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -5355,7 +5355,7 @@ export class Runtime { persistent_vars: persistent_memory ? Object.keys(persistent_memory): [], execution_permission: header?.executable, // allow execution? - impersonation_permission: Runtime.endpoint.equals(header?.sender), // at the moment: only allow endpoint to impersonate itself + impersonation_permission: Runtime.endpoint?.equals(header?.sender), // at the moment: only allow endpoint to impersonate itself inner_scope: null, // has to be copied from sub_scopes[0] @@ -5409,7 +5409,7 @@ export class Runtime { scope.header = header; scope.execution_permission = header?.executable // allow execution? - scope.impersonation_permission = Runtime.endpoint.equals(header?.sender) // at the moment: only allow endpoint to impersonate itself + scope.impersonation_permission = Runtime.endpoint?.equals(header?.sender) // at the moment: only allow endpoint to impersonate itself // enter outer scope ? if (scope.sub_scopes.length == 0) { From f0daef9598275347f85f8a995f5d19c62b842ce6 Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 23:09:46 +0100 Subject: [PATCH 47/56] add onclose listeners for websockts (again) (unrelated) --- network/communication-interfaces/websocket-interface.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/network/communication-interfaces/websocket-interface.ts b/network/communication-interfaces/websocket-interface.ts index 7edec920..a7bbc1d8 100644 --- a/network/communication-interfaces/websocket-interface.ts +++ b/network/communication-interfaces/websocket-interface.ts @@ -51,7 +51,7 @@ export abstract class WebSocketInterface extends CommunicationInterface { // don't trigger any further errorHandlers - // webSocket.removeEventListener('close', errorHandler); + webSocket.removeEventListener('close', errorHandler); webSocket.removeEventListener('error', errorHandler); this.#webSockets.delete(webSocket); @@ -59,6 +59,7 @@ export abstract class WebSocketInterface extends CommunicationInterface Date: Fri, 1 Mar 2024 23:10:00 +0100 Subject: [PATCH 48/56] wtf --- utils/auto_map.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/auto_map.ts b/utils/auto_map.ts index 127abc40..c4c2014f 100644 --- a/utils/auto_map.ts +++ b/utils/auto_map.ts @@ -9,8 +9,6 @@ const DEFAULT_CLASS_PRIMITIVE = Symbol('DEFAULT_CLASS_PRIMITIVE') const DEFAULT_CREATOR_FUNCTION = Symbol('DEFAULT_CREATOR_FUNCTION') const DEFAULT_VALUE = Symbol('DEFAULT_VALUE') -export const _ = "_"; - export type AutoMap = Map & { getAuto(key: K): V; enableAutoRemove(): AutoRemoveMap & AutoMap From 34315e312a6506a554c66a11ca7791c8958520b2 Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 23:11:23 +0100 Subject: [PATCH 49/56] cleanup legacy decorators, add new entrypoint, timeout decorators --- js_adapter/decorators.ts | 46 ++- js_adapter/js_class_adapter.ts | 590 +++----------------------------- js_adapter/legacy_decorators.ts | 328 ------------------ runtime/constants.ts | 1 + runtime/runtime.ts | 15 +- types/function.ts | 11 +- utils/interface-generator.ts | 1 - 7 files changed, 121 insertions(+), 871 deletions(-) delete mode 100644 js_adapter/legacy_decorators.ts diff --git a/js_adapter/decorators.ts b/js_adapter/decorators.ts index 568c9590..aa2816ef 100644 --- a/js_adapter/decorators.ts +++ b/js_adapter/decorators.ts @@ -18,7 +18,9 @@ export function handleClassDecoratorWithOptionalArgs<_Class extends Class, C ext export function handleClassDecoratorWithArgs<_Class extends Class, C extends ClassDecoratorContext<_Class>, const T extends unknown[], R>(args:T, callback: (arg: T, value: _Class, context: C)=>R): ((value: _Class, context: C) => R) { return (value: _Class, context: C) => callback(args, value, context) } - +export function handleClassMethodDecoratorWithArgs(args:T, callback: (arg: T, value:(...args:any[])=>any, context: C)=>R): ((value: (...args:any[])=>any, context: C) => R) { + return (value: (...args:any[])=>any, context: C) => callback(args, value, context) +} function isDecoratorContext(context: unknown) { return context && typeof context === "object" && "kind" in context @@ -28,6 +30,10 @@ function isDecoratorContext(context: unknown) { type PropertyDecoratorContext = ClassFieldDecoratorContext|ClassGetterDecoratorContext|ClassMethodDecoratorContextany)> +/** + * Marks a (static) class field as a property accessible by DATEX. + * @param type optional type for the property, must match the declared TypeScript type + */ export function property(type: string|Type|Class): (value: ((...args: any[])=>any)|undefined, context: PropertyDecoratorContext)=>void export function property(value: ((...args: any[])=>any)|undefined, context: PropertyDecoratorContext): void export function property(type: ((...args: any[])=>any)|undefined|string|Type|Class, context?: PropertyDecoratorContext) { @@ -37,13 +43,22 @@ export function property(type: ((...args: any[])=>any)|undefined|string|Type|Cla } +/** + * Adds an assertion to a class field that is checked before the field is set + * @returns + */ export function assert(assertion:(val:T)=>boolean|string|undefined): (value: undefined, context: ClassFieldDecoratorContext)=>void { return handleClassFieldDecoratorWithArgs([assertion], ([assertion], context) => { return Decorators.assert(assertion, context) }) } - +/** + * Make a class publicly accessible for an endpoint (only static methods and properties marked with @property are exposed) + * Also enables calling static class methods on other endpoints + * @param endpoint + * @param scope_name + */ export function endpoint(endpoint:target_clause|endpoint_name, scope_name?:string): (value: Class, context: ClassDecoratorContext)=>void export function endpoint(value: Class, context: ClassDecoratorContext): void export function endpoint(value: Class|target_clause|endpoint_name, context?: ClassDecoratorContext|string) { @@ -53,6 +68,33 @@ export function endpoint(value: Class|target_clause|endpoint_name, context?: Cla } /** + * Sets a class as the entrypoint for the current endpoint + */ +export function entrypoint(value: Class, context: ClassDecoratorContext) { + return Decorators.entrypoint(value, context) +} + +/** + * Adds a class as a property of entrypoint for the current endpoint + */ +export function entrypointProperty(value: Class, context: ClassDecoratorContext) { + return Decorators.entrypointProperty(value, context) +} + +/** + * Sets the maximum allowed time (in ms) for a remote function execution + * before a timeout error is thrown (default: 5s) + * @param timeMs timeout in ms + * @returns + */ +export function timeout(timeMs:number): (value: (...args:any[])=>any, context: ClassMethodDecoratorContext)=>void { + return handleClassMethodDecoratorWithArgs([timeMs], ([timeMs], _value, context) => { + return Decorators.timeout(timeMs, context) + }); +} + +/** + * Maps a class to a corresponding DATEX type * @deprecated Use struct(class {...}) instead; */ export function sync(type: string): (value: Class, context: ClassDecoratorContext)=>void diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index 8ae06573..3ce1fa71 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -15,14 +15,13 @@ import { Runtime, StaticScope } from "../runtime/runtime.ts"; import { Logger } from "../utils/logger.ts"; -import { Endpoint, endpoint_name, IdEndpoint, LOCAL_ENDPOINT, Target, target_clause } from "../types/addressing.ts"; +import { endpoint_name, LOCAL_ENDPOINT, Target, target_clause } from "../types/addressing.ts"; import { Type } from "../types/type.ts"; import { getProxyFunction, getProxyStaticValue, ObjectRef, Pointer, UpdateScheduler } from "../runtime/pointers.ts"; -import { Error as DatexError, ValueError } from "../types/errors.ts"; import { Function as DatexFunction } from "../types/function.ts"; import { DatexObject } from "../types/object.ts"; import { Tuple } from "../types/tuple.ts"; -import { DX_PERMISSIONS, DX_TYPE, DX_ROOT, INIT_PROPS, DX_EXTERNAL_SCOPE_NAME, DX_EXTERNAL_FUNCTION_NAME } from "../runtime/constants.ts"; +import { DX_PERMISSIONS, DX_TYPE, DX_ROOT, INIT_PROPS, DX_EXTERNAL_SCOPE_NAME, DX_EXTERNAL_FUNCTION_NAME, DX_TIMEOUT } 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"; @@ -50,38 +49,6 @@ export function instance(fromClassOrType:{new(...params:any[]):T}|Type, pr } -/** - * List of decorators - * - * @meta: mark method parameter that should contain meta data about the datex request / declare index of 'meta' parameter in method - * @docs: add docs to a static scope class / pseudo class - * - * @allow: define which endpoints have access to a class / method / property - * @to: define which on endpoints a method should be called / from which endpoint a property should be fetched - * - * @no_result: don't wait for result - * - * Static: - * @scope(name?:string): declare a class as a static scope, or add a static scope to a static property/method - * @root_extension: root extends this static scope in every executed DATEX scope (all static scope members become variables) - * @root_variable: static scope becomes a root variable in every executed DATEX scope (scope name is variable name) - * - * @remote: get a variable from a remote static scope or call a method in a remote static scope - * @expose: make a method/variable in a static scope available to others - * - * Sync: - * @sync: make a class syncable, or sync a property/method - * @sealed: make a sync class sealed, or seal individual properties/methods - * @anonymous: force prevent creating a pointer reference for an object, always transmit serialized - * - * @constructor: called after constructor, if instance is newly generated - * @generator: called after @constructor, if instance is newly generated - * @replicator: called after @constructor, if instance is a clone - * @destructor: called when pointer is garbage collected, or triggers garbage collection - */ - - - // handles all decorators export class Decorators { @@ -122,11 +89,6 @@ export class Decorators { static FROM_TYPE = Symbol("FROM_TYPE"); - static CONSTRUCTOR = Symbol("CONSTRUCTOR"); - static REPLICATOR = Symbol("REPLICATOR"); - static DESTRUCTOR = Symbol("DESTRUCTOR"); - - public static setMetadata(context:DecoratorContext, key:string|symbol, value:unknown) { if (!context.metadata[key]) context.metadata[key] = {} const data = context.metadata[key] as {public?:Record, constructor?:any} @@ -139,43 +101,6 @@ export class Decorators { } } - /** @expose(allow?:filter): make a method in a static scope available to be called by others */ - static public(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[target_clause?] = []) { - - // invalid decorator call - if (kind != "method" && kind != "field") logger.error("Cannot use @expose for value '" + name.toString() +"'"); - else if (!is_static) logger.error("Cannot use @expose for non-static field '" + name.toString() +"'"); - - // handle decorator - else { - setMetadata(Decorators.IS_EXPOSED, true) - if (params.length) Decorators.addMetaFilter( - params[0], - setMetadata, getMetadata, Decorators.ALLOW_FILTER - ) - } - } - - /** @namespace(name?:string): declare a class as a #public property */ - static namespace(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[string?] = []) { - - // invalid decorator call - if (!is_static && kind != "class") logger.error("Cannot use @scope for non-static field '" + name!.toString() +"'"); - - // handle decorator - else { - setMetadata(Decorators.NAMESPACE, params[0] ?? value?.name) - - // class @namespace - if (kind == "class") _old_publicStaticClass(value); - - // @namespace for static field -> @remote + @expose - else { - setMetadata(Decorators.IS_REMOTE, true) - setMetadata(Decorators.IS_EXPOSED, true) - } - } - } /** @endpoint(endpoint?:string|Datex.Endpoint, namespace?:string): declare a class as a #public property */ static endpoint(endpoint:target_clause|endpoint_name, scope_name:string|undefined, value: Class, context: ClassDecoratorContext) { @@ -192,53 +117,22 @@ export class Decorators { } // custom namespace name - this.setMetadata(context, Decorators.NAMESPACE, scope_name ?? value?.name); - registerPublicStaticClass(value, context.metadata); + this.setMetadata(context, Decorators.NAMESPACE, scope_name ?? value.name); + registerPublicStaticClass(value, 'public', context.metadata); } - /** @root_extension: root extends this static scope in every executed DATEX scope (all static scope members become variables) */ - static default(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { - - // invalid decorator call - if (!is_static && kind != "class") logger.error("Cannot use @root_extension for non-static field '" + name.toString() +"'"); - - // handle decorator - else { - setMetadata(Decorators.DEFAULT, true) - - if (kind == "class") _old_publicStaticClass(value); - } + /** @entrypoint is set as endpoint entrypoint */ + static entrypoint(value:Class, context: ClassDecoratorContext) { + this.setMetadata(context, Decorators.SEND_FILTER, true) // indicate to always use local endpoint (expose) + this.setMetadata(context, Decorators.NAMESPACE, value.name); + registerPublicStaticClass(value, 'entrypoint', context.metadata); } - /** @root_variable: static scope becomes a root variable in every executed DATEX scope (scope name is variable name) */ - static default_property(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { - - // invalid decorator call - if (!is_static && kind != "class") logger.error("Cannot use @root_variable for non-static field '" + name.toString() +"'"); - - // handle decorator - else { - setMetadata(Decorators.DEFAULT_PROPERTY, true) - - if (kind == "class") _old_publicStaticClass(value); - } - } - - /** @remote(from?:filter): get a variable from a static scope or call a function */ - static remote(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[target_clause?] = []) { - - // invalid decorator call - if (kind == "class") logger.error("Cannot use @remote for a class"); - else if (!is_static) logger.error("Cannot use @remote for non-static field '" + name.toString() +"'"); - - // handle decorator - else { - setMetadata(Decorators.IS_REMOTE, true) - if (params.length) Decorators.addMetaFilter( - params[0], - setMetadata, getMetadata, Decorators.SEND_FILTER - ) - } + /** @entrypointProperty is set as a property of the endpoint entrypoint */ + static entrypointProperty(value:Class, context: ClassDecoratorContext) { + this.setMetadata(context, Decorators.SEND_FILTER, true) // indicate to always use local endpoint (expose) + this.setMetadata(context, Decorators.NAMESPACE, value.name); + registerPublicStaticClass(value, 'entrypointProperty', context.metadata); } @@ -282,9 +176,9 @@ export class Decorators { } /** @timeout(msecs?:number): DATEX request timeout */ - static timeout(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[number?] = []) { - if (params[0] && params[0] > 2**31) throw new Error("@timeout: timeout too big (max value is 2^31), use Infinity if you want to disable the timeout") - setMetadata(Decorators.TIMEOUT, params[0]) + static timeout(timeMs:number, context:ClassMethodDecoratorContext) { + if (isFinite(timeMs) && timeMs > 2**31) throw new Error("@timeout: timeout too big (max value is 2^31), use Infinity if you want to disable the timeout") + this.setMetadata(context, Decorators.TIMEOUT, timeMs) } /** @allow(allow:filter): Allowed endpoints for class/method/field */ @@ -376,32 +270,6 @@ export class Decorators { } - /** - * @deprecated use \@sync - */ - static template(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[(string|Type)?] = []) { - if (kind != "class") logger.error("@template can only be used as a class decorator"); - - else { - //initPropertyTypeAssigner(); - - const original_class = value; - let type: Type; - - // get template type - if (typeof params[0] == "string" || params[0] instanceof Type) { - 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.replace(/^_/, '')); // remove leading _ from type name - - - // return new templated class - return createTemplateClass(original_class, type); - } - - } - /** @sync: sync class/property */ static sync(type: string|Type|undefined, value: Class, context?: ClassDecoratorContext) { @@ -436,68 +304,6 @@ export class Decorators { return createTemplateClass(originalClass, normalizedType, true, true, callerFile, context?.metadata); } - /** @sealed: sealed class/property */ - static sealed(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { - - // invalid decorator call - if (is_static) logger.error("Cannot use @sealed for static field '" + name.toString() +"'"); - - // handle decorator - else { - setMetadata(Decorators.IS_SEALED, true) - } - } - - /** @anonymous: anonymous class/property */ - static anonymous(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { - - // invalid decorator call - if (is_static) logger.error("Cannot use @anonymous for static field '" + name.toString() +"'"); - - // handle decorator - else { - setMetadata(Decorators.IS_ANONYMOUS, true) - } - } - - - /** @observe(handler:Function): listen to value changes */ - static observe(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[Function?] = []) { - setMetadata(Decorators.OBSERVER, params[0]) - } - - - - /** @anonymize: serialize return values (no pointers), only the first layer */ - static anonymize(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { - - // invalid decorator call - if (kind == "class") logger.error("Cannot use @anonymize for classes"); - - // handle decorator - else { - setMetadata(Decorators.ANONYMIZE, true) - } - } - - - /** @type(type:string|DatexType)/ (namespace:name) - * sync class with type */ - static type(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[(string|Type|Class)] = []) { - const type = normalizeType(params[0]); - setMetadata(Decorators.FORCE_TYPE, type) - } - - /** @from(type:string|DatexType): sync class from type */ - static from(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[(string|Type)?] = []) { - // invalid decorator call - if (kind !== "class") logger.error("Can use @from only for classes"); - - // handle decorator - else { - setMetadata(Decorators.FROM_TYPE, params[0]) - } - } /** @update(interval:number|scheduler:DatexUpdateScheduler): set update interval / scheduler */ static update(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[(number|UpdateScheduler)?] = []) { @@ -506,50 +312,6 @@ export class Decorators { else setMetadata(Decorators.SCHEDULER, new UpdateScheduler(params[0])); } - - - /** @constructor: called after constructor if newly generateds */ - static ["constructor"](value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { - - // invalid decorator call - if (is_static) logger.error("Cannot use @constructor for static field '" + name.toString() +"'"); - else if (kind != "method") logger.error("Cannot only use @constructor for methods"); - - // handle decorator - else { - setMetadata(Decorators.CONSTRUCTOR, true) - } - } - - - - /** @replicator: called after constructor if cloned */ - static replicator(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { - - // invalid decorator call - if (is_static) logger.error("Cannot use @replicator for static field '" + name.toString() +"'"); - else if (kind != "method") logger.error("Cannot only use @replicator for methods"); - - // handle decorator - else { - setMetadata(Decorators.REPLICATOR, true) - } - } - - /** @destructor: called after constructor if cloned */ - static destructor(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:undefined) { - - // invalid decorator call - if (is_static) logger.error("Cannot use @destructor for static field '" + name.toString() +"'"); - else if (kind != "method") logger.error("Cannot only use @destructor for methods"); - - // handle decorator - else { - setMetadata(Decorators.DESTRUCTOR, true) - } - } - - // handle ALLOW_FILTER for classes, methods and fields // adds filter private static addMetaFilter(new_filter:target_clause|endpoint_name, context: DecoratorContext, filter_symbol:symbol){ @@ -591,54 +353,72 @@ function normalizeType(type:Type|string|Class, allowTypeParams = true, defaultNa } } +type class_data = {name:string, static_scope:StaticScope, properties: string[], metadata:any} -const initialized_static_scope_classes = new Map(); - - -const registered_static_classes = new Set(); -function registerPublicStaticClass(publicClass:Class, metadata?:Record){ - registered_static_classes.add(publicClass); +const PROPERTY_COLLECTION = Symbol("PROPERTY_COLLECTION"); +const registeredClasses = new Map(); +const pendingClassRegistrations = new Map>().setAutoDefault(Set); - // init class (if endpoint already loaded) - initPublicStaticClass(publicClass, metadata) +function registerPublicStaticClass(publicClass:Class, type:'public'|'entrypoint'|'entrypointProperty', metadata?:Record){ + pendingClassRegistrations.getAuto(publicClass).add(type); + initPublicStaticClass(publicClass, type, metadata) } -type class_data = {name:string, static_scope:StaticScope, properties: string[], metadata:any} - export function initPublicStaticClasses(){ - for (const reg_class of registered_static_classes) { - initPublicStaticClass(reg_class) + for (const [reg_class, types] of [...pendingClassRegistrations]) { + for (const type of [...types]) { + initPublicStaticClass(reg_class, type) + } } + pendingClassRegistrations.clear(); } -function initPublicStaticClass(publicClass: Class, metadata?:Record) { +function initPublicStaticClass(publicClass: Class, type: 'public'|'entrypoint'|'entrypointProperty', metadata?:Record) { if (!Runtime.endpoint || Runtime.endpoint === LOCAL_ENDPOINT) return; - if (initialized_static_scope_classes.has(publicClass)) return; metadata ??= (publicClass)[METADATA]; if (!metadata) throw new Error(`Missing metadata for class ${publicClass.name}`) let targets = metadata[Decorators.SEND_FILTER]?.constructor; if (targets == true) targets = Runtime.endpoint; // use own endpoint per default - let data:any; + let data = registeredClasses.get(publicClass); // expose if current endpoint matches class endpoint if (Logical.matches(Runtime.endpoint, targets, Target)) { - data ??= getStaticClassData(publicClass, true, metadata); + data ??= getStaticClassData(publicClass, true, type == 'public', metadata); if (!data) throw new Error("Could not get data for static class") exposeStaticClass(publicClass, data); } // also enable remote access if not exactly and only the current endpoint if (Runtime.endpoint !== targets) { - data ??= getStaticClassData(publicClass, false, metadata); + data ??= getStaticClassData(publicClass, false, false, metadata); if (!data) throw new Error("Could not get data for static class") remoteStaticClass(publicClass, data, targets) } + // set method timeouts + for (const [method_name, timeout] of Object.entries(metadata[Decorators.TIMEOUT]?.public??{})) { + const method = (publicClass as any)[method_name]; + if (method) method[DX_TIMEOUT] = timeout + } + + // set entrypoint + if (type == 'entrypoint') { + data ??= getStaticClassData(publicClass, true, false, metadata); + if (Runtime.endpoint_entrypoint) logger.error("Existing entrypoint was overridden with @entrypoint class " + publicClass.name); + Runtime.endpoint_entrypoint = data.static_scope; + } + else if (type == 'entrypointProperty') { + data ??= getStaticClassData(publicClass, true, false, metadata); + if (Runtime.endpoint_entrypoint == undefined) Runtime.endpoint_entrypoint = {[PROPERTY_COLLECTION]:true} + if (typeof Runtime.endpoint_entrypoint !== "object" || !Runtime.endpoint_entrypoint[PROPERTY_COLLECTION]) logger.error("Cannot set endpoint property " + publicClass.name + ". The entrypoint is already set to another value."); + Runtime.endpoint_entrypoint[publicClass.name] = data.static_scope; + } - DatexObject.seal(data.static_scope); - initialized_static_scope_classes.set(publicClass, data.static_scope); + // DatexObject.seal(data.static_scope); + registeredClasses.set(publicClass, data); + pendingClassRegistrations.get(publicClass)!.delete(type); } @@ -719,7 +499,6 @@ function remoteStaticClass(original_class:Class, data:class_data, targets:target const timeout_public = data.metadata[Decorators.TIMEOUT]?.public; const timeout_private = data.metadata[Decorators.TIMEOUT]?.private; - // prototype for all options objects of static proxy methods (Contains the dynamic_filter) let options_prototype: {[key:string]:any} = {}; @@ -728,7 +507,7 @@ function remoteStaticClass(original_class:Class, data:class_data, targets:target Object.defineProperty(original_class, 'to', { value: function(...targets:(Target|endpoint_name)[]){ options_prototype.dynamic_filter = new Disjunction(); - for (let target of targets) { + for (const target of targets) { if (typeof target == "string") options_prototype.dynamic_filter.add(Target.get(target)) else options_prototype.dynamic_filter.add(target) } @@ -776,7 +555,7 @@ function remoteStaticClass(original_class:Class, data:class_data, targets:target } -function getStaticClassData(original_class:Class, staticScope = true, metadata?:Record) { +function getStaticClassData(original_class:Class, staticScope = true, expose = true, metadata?:Record) { metadata ??= (original_class)[METADATA]; if (!metadata) return; const static_scope_name = typeof metadata[Decorators.NAMESPACE]?.constructor == 'string' ? metadata[Decorators.NAMESPACE]?.constructor : original_class.name; @@ -784,7 +563,7 @@ function getStaticClassData(original_class:Class, staticScope = true, metadata?: return { metadata, - static_scope: staticScope ? new StaticScope(static_scope_name) : null, + static_scope: staticScope ? StaticScope.get(static_scope_name, expose) : null, name: static_scope_name, properties: static_properties } @@ -792,257 +571,6 @@ function getStaticClassData(original_class:Class, staticScope = true, metadata?: -function _old_publicStaticClass(original_class:Class) { - - // already initialized - if (initialized_static_scope_classes.has(original_class)) { - - // is default property - if (original_class[METADATA]?.[Decorators.DEFAULT_PROPERTY]?.constructor) { - const static_scope = initialized_static_scope_classes.get(original_class); - if (!Runtime.endpoint_entrypoint || typeof Runtime.endpoint_entrypoint != "object") Runtime.endpoint_entrypoint = {}; - Runtime.endpoint_entrypoint[static_scope.name] = static_scope - } - // is default value - if (original_class[METADATA]?.[Decorators.DEFAULT]?.constructor) { - const static_scope = initialized_static_scope_classes.get(original_class); - Runtime.endpoint_entrypoint = static_scope; - } - - - return; - } - - - let static_properties = Object.getOwnPropertyNames(original_class) - - const metadata = original_class[METADATA]; - if (!metadata) return; - - // prototype for all options objects of static proxy methods (Contains the dynamic_filter) - let options_prototype: {[key:string]:any} = {}; - - const static_scope_name = typeof metadata[Decorators.NAMESPACE]?.constructor == 'string' ? metadata[Decorators.NAMESPACE]?.constructor : original_class.name; - let static_scope:StaticScope; - - // add builtin methods - - Object.defineProperty(original_class, 'to', { - value: function(...targets:(Target|endpoint_name)[]){ - options_prototype.dynamic_filter = new Disjunction(); - for (let target of targets) { - if (typeof target == "string") options_prototype.dynamic_filter.add(Target.get(target)) - else options_prototype.dynamic_filter.add(target) - } - return this; - }, - configurable: false, - enumerable: false, - writable: false - }); - /* - target.list = async function (...filters:ft[]|[Datex.filter]) { - if (!DATEX_CLASS_ENDPOINTS.has(this)) return false; - DATEX_CLASS_ENDPOINTS.get(this).dynamic_filter.appenddatex_filter(...filters) - return (await DATEX_CLASS_ENDPOINTS.get(this).__sendHandler("::list"))?.data || new Set(); - } - target.on_result = function(call: (data:datex_res, meta:{station_id:number, station_bundle:number[]})=>any){ - if (!DATEX_CLASS_ENDPOINTS.has(this)) return this; - DATEX_CLASS_ENDPOINTS.get(this).current_dynamic_callback = call; - return this; - } - target.no_result = function() { - if (!DATEX_CLASS_ENDPOINTS.has(this)) return this; - DATEX_CLASS_ENDPOINTS.get(this).current_no_result = true; - return this; - } - - target.ping = async function(...filters:ft[]|[Datex.filter]){ - if (!DATEX_CLASS_ENDPOINTS.has(this)) return false; - return new Promise(resolve=>{ - DATEX_CLASS_ENDPOINTS.get(this).dynamic_filter.appenddatex_filter(...filters) - let pings = {} - let start_time = new Date().getTime(); - DATEX_CLASS_ENDPOINTS.get(this).current_dynamic_callback = (data, meta) => { - pings[meta.station_id] = (new Date().getTime() - start_time) + "ms"; - if (Object.keys(pings).length == meta.station_bundle.length) resolve(pings); - } - setTimeout(()=>resolve(pings), 10000); - DATEX_CLASS_ENDPOINTS.get(this).__sendHandler("::ping") - }) - } - - target.self = function(){ - if (!DATEX_CLASS_ENDPOINTS.has(this)) return false; - DATEX_CLASS_ENDPOINTS.get(this).dynamic_self = true; - } - target.encrypt = function(encrypt=true){ - if(DATEX_CLASS_ENDPOINTS.has(this)) DATEX_CLASS_ENDPOINTS.get(this).current_encrypt = encrypt; - return this; - } - */ - - let class_send_filter:target_clause = metadata[Decorators.SEND_FILTER]?.constructor - // @ts-ignore - if (class_send_filter == Object) class_send_filter = undefined; - let class_allow_filter:target_clause = metadata[Decorators.ALLOW_FILTER]?.constructor - // @ts-ignore - if (class_allow_filter == Object) class_allow_filter = undefined; - - // per-property metadata - const exposed_public = metadata[Decorators.IS_EXPOSED]?.public; - const exposed_private = metadata[Decorators.IS_EXPOSED]?.private; - - const remote_public = metadata[Decorators.IS_REMOTE]?.public; - const remote_private = metadata[Decorators.IS_REMOTE]?.private; - const timeout_public = metadata[Decorators.TIMEOUT]?.public; - const timeout_private = metadata[Decorators.TIMEOUT]?.private; - const send_filter = metadata[Decorators.SEND_FILTER]?.public; - - - for (const name of static_properties) { - - const current_value = original_class[name]; - - // expose - if ((exposed_public?.hasOwnProperty(name) && exposed_public[name]) || (exposed_private?.hasOwnProperty(name) && exposed_private[name])) { - - if (!static_scope) static_scope = StaticScope.get(static_scope_name) - - // function - if (typeof current_value == "function") { - // set allowed endpoints for this method - //static_scope.setAllowedEndpointsForProperty(name, this.method_a_filters.get(name)) - - const dx_function = Pointer.proxifyValue(DatexFunction.createFromJSFunction(current_value, original_class, name), true, undefined, false, true) ; // generate - - // public function - const ptr = Pointer.pointerifyValue(dx_function); - if (ptr instanceof Pointer) ptr.grantPublicAccess(true); - - static_scope.setVariable(name, dx_function); // add to static scope - } - - // field - else { - // set static value (datexified) - const setProxifiedValue = (val:any) => { - static_scope.setVariable(name, Pointer.proxifyValue(val, true, undefined, false, true)) - // public function - const ptr = Pointer.proxifyValue(val); - if (ptr instanceof Pointer) ptr.grantPublicAccess(true); - }; - setProxifiedValue(current_value); - - /*** handle new value assignments to this property: **/ - - // similar to addObjProxy in DatexRuntime / DatexPointer - const property_descriptor = Object.getOwnPropertyDescriptor(original_class, name); - - // add original getters/setters to static_scope if they exist - if (property_descriptor?.set || property_descriptor?.get) { - Object.defineProperty(static_scope, name, { - set: val => { - property_descriptor.set?.call(original_class,val); - }, - get: () => { - return property_descriptor.get?.call(original_class); - } - }); - } - - // new getter + setter - Object.defineProperty(original_class, name, { - get:()=>static_scope.getVariable(name), - set:(val)=>setProxifiedValue(val) - }); - } - } - - // remote - - if ((remote_public?.hasOwnProperty(name) && remote_public[name]) || (remote_private?.hasOwnProperty(name) && remote_private[name])) { - - const timeout = timeout_public?.[name]??timeout_private?.[name]; - const filter = new Conjunction(class_send_filter, send_filter?.[name]); - - // function - if (typeof current_value == "function") { - const options = Object.create(options_prototype); - Object.assign(options, {filter, sign:true, scope_name:static_scope_name, timeout}); - const proxy_fn = getProxyFunction(name, options); - Object.defineProperty(original_class, name, {value:proxy_fn}) - } - - // field - else { - const options = Object.create(options_prototype); - Object.assign(options, {filter, sign:true, scope_name:static_scope_name, timeout}); - const proxy_fn = getProxyStaticValue(name, options); - Object.defineProperty(original_class, name, { - get: proxy_fn // set proxy function for getting static value - }); - } - } - - } - - - // each methods - - //const each_private = original_class.prototype[METADATA]?.[Decorators.IS_EACH]?.private; - const each_public = original_class[METADATA]?.[Decorators.IS_EACH]?.public; - - let each_scope: any; - - for (let [name, is_each] of Object.entries(each_public??{})) { - if (!is_each) continue; - - if (!static_scope) static_scope = StaticScope.get(static_scope_name) - - // add _e to current static scope - if (!each_scope) { - each_scope = {}; - static_scope.setVariable("_e", each_scope); // add to static scope - } - - let method:Function = original_class.prototype[name]; - let type = Type.getClassDatexType(original_class); - - if (typeof method != "function") throw new DatexError("@each can only be used with functions") - - - - /****** expose _e */ - // let meta_index = getMetaParamIndex(original_class.prototype, name); - // if (typeof meta_index == "number") meta_index ++; // shift meta_index (insert 'this' before) - - let proxy_method = function(_this:any, ...args:any[]) { - if (!(_this instanceof original_class)) { - console.warn(_this, args); - throw new ValueError("Invalid argument 'this': type should be " + type) - } - return method.call(_this, ...args) - }; - // add ' this' as first argument - //params?.unshift([type, "this"]) - - let dx_function = Pointer.proxifyValue(DatexFunction.createFromJSFunction(proxy_method, original_class, name), true, undefined, false, true) ; // generate - - each_scope[name] = dx_function // add to static scope - - } - - - // finally seal the static scope - if (static_scope) { - DatexObject.seal(static_scope); - initialized_static_scope_classes.set(original_class, static_scope); - } -} - - - const templated_classes = new Map() // original class, templated class export function createTemplateClass(original_class: Class, type:Type, sync = true, add_js_interface = true, callerFile?:string, metadata?:Record){ @@ -1108,10 +636,6 @@ export function createTemplateClass(original_class: Class, type:Type, sync = tru type.setTemplate(template) - - // has static scope methods? - _old_publicStaticClass(original_class); - // create shadow class extending the actual class const sync_auto_cast_class = proxyClass(original_class, type, metadata?.[Decorators.IS_SYNC]?.constructor ?? sync) diff --git a/js_adapter/legacy_decorators.ts b/js_adapter/legacy_decorators.ts deleted file mode 100644 index 226b1a50..00000000 --- a/js_adapter/legacy_decorators.ts +++ /dev/null @@ -1,328 +0,0 @@ -/** - ╔══════════════════════════════════════════════════════════════════════════════════════╗ - ║ Typescript Legacy Decorators for the DATEX JS Interface ║ - ║ - Use until the JS decorator proposal (TC39 Stage 2) is fully implemented ║ - ╠══════════════════════════════════════════════════════════════════════════════════════╣ - ║ Unyt core library ║ - ║ Visit docs.unyt.org/unyt_js for more information ║ - ╠═════════════════════════════════════════╦════════════════════════════════════════════╣ - ║ © 2021 unyt.org ║ ║ - ╚═════════════════════════════════════════╩════════════════════════════════════════════╝ - */ - -import { Decorators, METADATA } from "./js_class_adapter.ts"; -import { } from "../runtime/runtime.ts"; -import { endpoint_name, Target, target_clause } from "../types/addressing.ts"; -import { Type } from "../types/type.ts"; -import { UpdateScheduler, Pointer } from "../runtime/pointers.ts"; -import type { Class } from "../utils/global_types.ts"; - -// decorator types -export type context_kind = 'class'|'method'|'getter'|'setter'|'field'|'auto-accessor'; -export type context_name = string|symbol|undefined; -export type context_meta_setter = (key:symbol, value:any) => void -export type context_meta_getter = (key:symbol ) => any - -type decorator_target = {[key: string]: any} & Partial, never>>; -type decorator_target_optional_params = decorator_target | Function; // only working for static methods! - -const __metadataPrivate = new WeakMap(); -const createObjectWithPrototype = (obj:object, key:any) => Object.hasOwnProperty.call(obj, key) ? obj[key] : Object.create(obj[key] || Object.prototype); - - -// get context kind (currently only supports class, method, field) -function getContextKind(args:any[]):context_kind { - if (typeof args[0] == "function" && args[1] == null && args[2] == null) return 'class'; - if ((typeof args[0] == "function" || typeof args[0] == "object") && (typeof args[2] == "function" || typeof args[2]?.value == "function")) return 'method'; - if ((typeof args[0] == "function" || typeof args[0] == "object") && typeof args[1] == "string") return 'field'; -} -// is context static field/method? -function isContextStatic(args:any[]):boolean { - return typeof args[0] == "function" && args[1] != null; -} - - -// add optional arguments, then call JS Interface decorator handler -export function handleDecoratorArgs(args:any[], method:(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params?:any[]) => any, first_argument_is_function = false):(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor) => any { - let kind = getContextKind(args); - // is @decorator(x,y,z) - if (!kind || first_argument_is_function) { - // inject args as decorator params - const params = args; // x,y,z - return (...args:any[]) => { - let kind = getContextKind(args); - // same as below (in else), + params - let is_static = isContextStatic(args); - let target = args[0]; - let name = kind == 'class' ? args[0].name : args[1]; - let value = kind == 'class' ? args[0] : args[2]?.value; - let meta_setter = createMetadataSetter(target, name, kind == 'class'); - let meta_getter = createMetadataGetter(target, name, kind == 'class'); - //console.log("@"+method.name + " name: " + name + ", kind: " + kind + ", is_static:" + is_static + ", params:", params, value) - return method(value, name, kind, is_static, false, meta_setter, meta_getter, params); - } - } - // is direct @decorator - else { - let is_static = isContextStatic(args); - let target = args[0]; - let name = kind == 'class' ? args[0].name : args[1]; - let value = kind == 'class' ? args[0] : args[2]?.value; - let meta_setter = createMetadataSetter(target, name, kind == 'class'); - let meta_getter = createMetadataGetter(target, name, kind == 'class'); - //console.log("@"+method.name + " name: " + name + ", kind: " + kind + ", is_static:" + is_static, value) - return method(value, name, kind, is_static, false, meta_setter, meta_getter); - } -} - -function createMetadataSetter(target:Function, name:string, is_constructor = false, is_private=false) { - return (key:symbol, value:unknown)=>{ - if (typeof key !== "symbol") { - throw new TypeError("the key must be a Symbol"); - } - - target[METADATA] = createObjectWithPrototype(target, METADATA); - target[METADATA][key] = createObjectWithPrototype(target[METADATA], key); - target[METADATA][key].public = createObjectWithPrototype(target[METADATA][key], "public"); - - if (!Object.hasOwnProperty.call(target[METADATA][key], "private")) { - Object.defineProperty(target[METADATA][key], "private", { - get() { - return Object.values(__metadataPrivate.get(target[METADATA][key]) || {}).concat(Object.getPrototypeOf(target[METADATA][key])?.private || []); - } - }); - } - // constructor - if (is_constructor) { - target[METADATA][key].constructor = value; - } - // private - else if (is_private) { - if (!__metadataPrivate.has(target[METADATA][key])) { - __metadataPrivate.set(target[METADATA][key], {}); - } - __metadataPrivate.get(target[METADATA][key])[name] = value; - } - // public - else { - target[METADATA][key].public[name] = value; - } - } -} -function createMetadataGetter(target:Function, name:string, is_constructor = false, is_private=false) { - return (key:symbol) => { - if (target[METADATA] && target[METADATA][key]) { - if (is_constructor) return target[METADATA][key]["constructor"]?.[name]; - else if (is_private) return (__metadataPrivate.has(target[METADATA][key]) ? __metadataPrivate.get(target[METADATA][key])?.[name] : undefined) - else return target[METADATA][key].public?.[name] - } - } -} - -// legacy decorator functions - -// @deprecated -// TODO: remove, use @property (also for static methods) -export function expose(allow?: target_clause):any -export function expose(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor):any -export function expose(...args:any[]) { - return handleDecoratorArgs(args, Decorators.public); -} - - - -// @deprecated -// TODO: remove, use endpoint -export function scope(name:string):any -export function scope(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor):any -export function scope(...args:any[]) { - return handleDecoratorArgs(args, Decorators.namespace); -} - -// @deprecated -// TODO: remove, use endpoint -export function namespace(name:string):any -export function namespace(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor):any -export function namespace(...args:any[]) { - return handleDecoratorArgs(args, Decorators.namespace); -} - -// use instead of @namespace @to -export function endpoint(endpoint:target_clause|endpoint_name, scope_name?:string):any -export function endpoint(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor):any -export function endpoint(...args:any[]) { - return handleDecoratorArgs(args, Decorators.endpoint); -} - - -export function endpoint_default(target: any, name?: string, method?:any):any -export function endpoint_default(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.default); -} - -export function default_property(target: any, name?: string, method?:any):any -export function default_property(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.default_property); -} - -// @deprecated -// TODO: remove, use @property (also for static methods) -export function remote(from?: target_clause):any -export function remote(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_: string) -export function remote(...args:any[]) { - return handleDecoratorArgs(args, Decorators.remote); -} - - -export function docs(content: string):any -export function docs(...args:any[]) { - return handleDecoratorArgs(args, Decorators.docs); -} - -export function meta(index: number):any -export function meta(target: any, name?: string, method?:any) -export function meta(...args:any[]) { - return handleDecoratorArgs(args, Decorators.meta); -} - -export function sign(sign: boolean):any -export function sign(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor) -export function sign(...args:any[]) { - return handleDecoratorArgs(args, Decorators.sign); -} - -export function encrypt(encrypt: boolean):any -export function encrypt(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor) -export function encrypt(...args:any[]) { - return handleDecoratorArgs(args, Decorators.encrypt); -} - -export function no_result(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor) -export function no_result(...args:any[]) { - return handleDecoratorArgs(args, Decorators.no_result); -} - -export function timeout(msecs: number):any -export function timeout(...args:any[]) { - return handleDecoratorArgs(args, Decorators.timeout); -} - -export function allow(allow?: target_clause):any -export function allow(...args:any[]) { - return handleDecoratorArgs(args, Decorators.allow); -} - -export function to(to?: target_clause|endpoint_name):any -export function to(...args:any[]) { - return handleDecoratorArgs(args, Decorators.to); -} - - -export function sealed(target: any, name?: string, method?:any) -export function sealed(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.sealed); -} - - -export function each(target: any, name?: string, method?:any) -export function each(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.each); -} - -export function sync(type:string):any -export function sync(target: any, name?: string, method?:any):any -export function sync(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.sync); -} - -/** - * @deprecated use \@sync - */ -export function template(type:string):any -/** - * @deprecated use \@sync - */ -export function template(target: any, name?: string, method?:any):any -export function template(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.template); -} - -// export function property(name:string|number):any -export function property(type:string|Type|Class):any -export function property(target: any, name: string, method?:any):any -export function property(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.property, args[0] && typeof args[0] == "function"); -} - -export function jsdoc(target: any, name?: string, method?:any):any -export function jsdoc(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.jsdoc); -} - - -export function serialize(serializer:(parent:any, value:any)=>any):any -export function serialize(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.serialize, true); -} - - -export function observe(handler:Function):any -export function observe(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.observe); -} - -export function anonymize(_invalid_param_0_: decorator_target_optional_params, _invalid_param_1_?: string, _invalid_param_2_?: PropertyDescriptor) -export function anonymize(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.anonymize); -} - -export function anonymous(_target: T):T -export function anonymous(target: any, name?: string, method?: PropertyDescriptor) -export function anonymous(...args:any[]) { - // no decorator, but function encapsulating object to make it syncable (proxy) - if (args[0]==undefined || args[0] == null || (args[1]===undefined && args[0] && typeof args[0] == "object")) { - return Pointer.create(null, args[0], /*TODO*/false, undefined, false, true).val; - } - // decorator - return handleDecoratorArgs(args, Decorators.anonymous); -} - - -export function type(type:string|Type|Class):any -export function type(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.type, true); -} - -export function assert(assertion:(val:any)=>boolean|string|undefined|null):any -export function assert(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.assert, true); -} - - -export function from(type:string|Type):any -export function from(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.from); -} - -export function update(interval:number):any -export function update(scheduler:UpdateScheduler):any -export function update(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.update); -} - -// special sync class methods -export function constructor(target: any, propertyKey: string, descriptor: PropertyDescriptor) -export function constructor(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.constructor); -} - -export function replicator(target: any, propertyKey: string, descriptor: PropertyDescriptor) -export function replicator(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.replicator); -} - -export function destructor(target: any, propertyKey: string, descriptor: PropertyDescriptor) -export function destructor(...args:any[]): any { - return handleDecoratorArgs(args, Decorators.destructor); -} - diff --git a/runtime/constants.ts b/runtime/constants.ts index 434e6922..9f65bd55 100644 --- a/runtime/constants.ts +++ b/runtime/constants.ts @@ -13,6 +13,7 @@ export const DX_PTR: unique symbol = Symbol("DX_PTR"); // key for pointer object export const DX_TYPE: unique symbol = Symbol("DX_TYPE"); export const DX_ROOT: unique symbol = Symbol("DX_ROOT"); export const DX_SERIALIZED: unique symbol = Symbol("DX_SERIALIZED"); +export const DX_TIMEOUT: unique symbol = Symbol("DX_TIMEOUT"); // timeout for remote function execution export const DX_VALUE: unique symbol = Symbol("DX_VALUE"); export const DX_REPLACE: unique symbol = Symbol("DX_REPLACE"); // value that is used as a replacement when serializing export const DX_SOURCE: unique symbol = Symbol("DX_SOURCE"); // used to override the default loading behaviour for a pointer (fetching by id). Can be an arbitrary DATEX Script that can be resolved with datex.get. Currently only used by the interface generator for JS modules. diff --git a/runtime/runtime.ts b/runtime/runtime.ts index 0fe1995d..64cc3da3 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -105,6 +105,7 @@ RuntimePerformance.marker("module loading time", "modules_loaded", "runtime_star // TODO reader for node.js const ReadableStreamDefaultReader = globalThis.ReadableStreamDefaultReader ?? class {}; +const EXPOSE = Symbol("EXPOSE"); export class StaticScope { @@ -115,11 +116,15 @@ export class StaticScope { public static readonly DOCS: unique symbol = Symbol("docs"); // return a scope with a given name, if it already exists - public static get(name?:string):StaticScope { - return this.scopes.get(name) || new StaticScope(name); + public static get(name?:string, expose = true): StaticScope { + if (!expose) return new StaticScope(name, false); + else return this.scopes.get(name) || new StaticScope(name); } - private constructor(name?:string){ + [EXPOSE]: boolean + + private constructor(name?:string, expose = true){ + this[EXPOSE] = expose; const proxy = Pointer.proxifyValue(this, false, undefined, false); DatexObject.setWritePermission(>proxy, undefined); // make readonly @@ -143,9 +148,9 @@ export class StaticScope { // update/set the name of this static scope set name(name:string){ - if (this[StaticScope.NAME]) StaticScope.scopes.delete(this[StaticScope.NAME]); + if (this[StaticScope.NAME] && this[EXPOSE]) StaticScope.scopes.delete(this[StaticScope.NAME]); this[StaticScope.NAME] = name; - StaticScope.scopes.set(this[StaticScope.NAME], this); + if (this[EXPOSE]) StaticScope.scopes.set(this[StaticScope.NAME], this); if (this[StaticScope.NAME] == "std") StaticScope.STD = this; } diff --git a/types/function.ts b/types/function.ts index ca909f04..6a3d24b5 100644 --- a/types/function.ts +++ b/types/function.ts @@ -11,7 +11,7 @@ import { Compiler } from "../compiler/compiler.ts"; import { Stream } from "./stream.ts" import { PermissionError, RuntimeError, TypeError, ValueError } from "./errors.ts"; import { ProtocolDataType } from "../compiler/protocol_types.ts"; -import { DX_EXTERNAL_FUNCTION_NAME, DX_EXTERNAL_SCOPE_NAME, VOID } from "../runtime/constants.ts"; +import { DX_EXTERNAL_FUNCTION_NAME, DX_EXTERNAL_SCOPE_NAME, DX_TIMEOUT, VOID } from "../runtime/constants.ts"; import { Type, type_clause } from "./type.ts"; import { callWithMetadata, callWithMetadataAsync, getMeta } from "../utils/caller_metadata.ts"; import { Datex } from "../mod.ts"; @@ -511,7 +511,14 @@ function to (this:Function, receivers:Receiver) { apply: (target, _thisArg, argArray:unknown[]) => { const externalScopeName = target[DX_EXTERNAL_SCOPE_NAME]; const externalFunctionName = target[DX_EXTERNAL_FUNCTION_NAME]; - return datex(`#public.?.? ?`, [externalScopeName, externalFunctionName, new Tuple(argArray)], receivers as target_clause) + const timeout = target[DX_TIMEOUT]; + return datex( + `#public.?.? ?`, + [externalScopeName, externalFunctionName, new Tuple(argArray)], + receivers as target_clause, + undefined, undefined, undefined, undefined, + timeout + ) } }) } diff --git a/utils/interface-generator.ts b/utils/interface-generator.ts index fee70b73..4c1148c1 100644 --- a/utils/interface-generator.ts +++ b/utils/interface-generator.ts @@ -230,7 +230,6 @@ function getClassTSCode(name:string, interf: interf, no_pointer = false) { const meta_is_sync = metadata[Datex.Decorators.IS_SYNC]?.constructor; const meta_is_sealed = metadata[Datex.Decorators.IS_SEALED]?.constructor; const meta_timeout = metadata[Datex.Decorators.TIMEOUT]?.public; - const meta_meta_index = metadata[Datex.Decorators.META_INDEX]?.public; let fields = ""; From 78ce1971c2222047be6ac6ad4983ef6646b95600 Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 1 Mar 2024 23:45:27 +0100 Subject: [PATCH 50/56] start work on webrtc interface --- datex_short.ts | 11 ++- .../webrtc-interface.ts | 75 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 network/communication-interfaces/webrtc-interface.ts diff --git a/datex_short.ts b/datex_short.ts index c2661cf6..e5fea4d9 100644 --- a/datex_short.ts +++ b/datex_short.ts @@ -4,7 +4,7 @@ import { baseURL, Runtime, PrecompiledDXB, Type, Pointer, Ref, PointerProperty, primitive, Target, IdEndpoint, Markdown, 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, dc } from "./datex_all.ts"; /** make decorators global */ -import { assert as _assert, property as _property, struct as _struct, endpoint as _endpoint, sync as _sync} from "./datex_all.ts"; +import { assert as _assert, timeout as _timeout, entrypoint as _entrypoint, entrypointProperty as _entrypointProperty, property as _property, struct as _struct, endpoint as _endpoint, sync as _sync} from "./datex_all.ts"; import { effect as _effect, always as _always, reactiveFn as _reactiveFn, asyncAlways as _asyncAlways, 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"; @@ -24,6 +24,9 @@ declare global { const struct: typeof _struct; const endpoint: typeof _endpoint; + const entrypoint: typeof _entrypoint; + const entrypointProperty: typeof _entrypointProperty; + const timeout: typeof _timeout; const always: typeof _always; /** * @deprecated Use struct(class {...}) instead; @@ -60,6 +63,12 @@ globalThis.struct = _struct; // @ts-ignore global globalThis.endpoint = _endpoint; // @ts-ignore global +globalThis.entrypoint = _entrypoint; +// @ts-ignore global +globalThis.entrypointProperty = _entrypointProperty; +// @ts-ignore global +globalThis.timeout = _timeout; +// @ts-ignore global globalThis.sync = _sync; // can be used instead of import(), calls a DATEX get instruction, works for urls, endpoint, ... diff --git a/network/communication-interfaces/webrtc-interface.ts b/network/communication-interfaces/webrtc-interface.ts new file mode 100644 index 00000000..2f957919 --- /dev/null +++ b/network/communication-interfaces/webrtc-interface.ts @@ -0,0 +1,75 @@ +import { Endpoint } from "../../types/addressing.ts"; +import { CommunicationInterface, CommunicationInterfaceSocket, InterfaceDirection, InterfaceProperties } from "../communication-interface.ts"; + +@endpoint class WebRTCSignaling { + + @property static offer(data:any) { + InterfaceManager.connect("webrtc", datex.meta!.sender, [data]); + } + + @property static accept(data:any) { + WebRTCClientInterface.waiting_interfaces_by_endpoint.get(datex.meta!.sender)?.setRemoteDescription(data); + } + + @property static candidate(data:any) { + WebRTCClientInterface.waiting_interfaces_by_endpoint.get(datex.meta!.sender)?.addICECandidate(data); + } +} + +export class WebRTCInterfaceSocket extends CommunicationInterfaceSocket { + + handleReceive = (event: MessageEvent) => { + if (event.data instanceof ArrayBuffer) { + this.receive(event.data) + } + } + + open() { + this.worker.addEventListener("message", this.handleReceive); + } + + close() { + this.worker.removeEventListener('message', this.handleReceive); + } + + send(dxb: ArrayBuffer) { + try { + this.worker.postMessage(dxb) + return true; + } + catch { + return false; + } + } +} + +/** + * Creates a direct DATEX communication channel between two WebRTC clients + */ +export class WebRTCInterface extends CommunicationInterface { + + public properties: InterfaceProperties = { + type: "webrtc", + direction: InterfaceDirection.IN_OUT, + latency: 20, + bandwidth: 50_000 + } + + constructor(endpoint: Endpoint) { + super() + const socket = new WebRTCInterfaceSocket(); + socket.endpoint = endpoint; + this.addSocket(socket); + } + + connect() { + return true; + } + + disconnect() {} + + cloneSocket(_socket: WebRTCInterfaceSocket) { + return new WebRTCInterfaceSocket(); + } + +} \ No newline at end of file From 694e58ee938a83b65ee936343f0b2a6f865326d8 Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 2 Mar 2024 00:40:03 +0100 Subject: [PATCH 51/56] fix decorator caller files --- js_adapter/js_class_adapter.ts | 8 +++----- types/struct.ts | 8 +++++++- types/type.ts | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index 3ce1fa71..21869017 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -271,7 +271,7 @@ export class Decorators { /** @sync: sync class/property */ - static sync(type: string|Type|undefined, value: Class, context?: ClassDecoratorContext) { + static sync(type: string|Type|undefined, value: Class, context?: ClassDecoratorContext, callerFile?:string) { if (context) { this.setMetadata(context ?? {kind: "class", metadata:(value as any)[METADATA]}, Decorators.IS_SYNC, true) @@ -291,10 +291,8 @@ export class Decorators { ) normalizedType = originalClass[METADATA]?.[Decorators.FORCE_TYPE]?.constructor else normalizedType = Type.get("ext", originalClass.name.replace(/^_/, '')); // remove leading _ from type name - let callerFile:string|undefined; - - if (client_type == "deno" && normalizedType.namespace !== "std") { - callerFile = getCallerInfo()?.[2]?.file ?? undefined; + if (!callerFile && client_type == "deno" && normalizedType.namespace !== "std") { + callerFile = getCallerInfo()?.[3]?.file ?? undefined; if (!callerFile) { logger.error("Could not determine JS module URL for type '" + normalizedType + "'") } diff --git a/types/struct.ts b/types/struct.ts index c7bc2f0b..5448c2a4 100644 --- a/types/struct.ts +++ b/types/struct.ts @@ -5,6 +5,8 @@ import { Class } from "../utils/global_types.ts"; import { sha256 } from "../utils/sha256.ts"; import { Type } from "./type.ts"; import { Decorators } from "../js_adapter/js_class_adapter.ts"; +import { getCallerFile } from "../utils/caller_metadata.ts"; +import { client_type } from "../utils/constants.ts"; type StructuralTypeDefIn = { [key: string]: Type|(new () => unknown)|StructuralTypeDefIn @@ -72,13 +74,15 @@ export function struct(def: Def): Type extends ExtensibleFunction { // never call the constructor directly!! should be private constructor(namespace?:string, name?:string, variation?:string, parameters?:any[]) { - super(namespace && namespace != "std" ? (val:any) => this.cast(val) : undefined) + super(namespace && namespace != "std" ? (val:any) => this.cast(val, undefined, undefined, true) : undefined) if (name) this.name = name; if (namespace) this.namespace = namespace; if (variation) this.variation = variation; From 17420d22172480602a58ed60019e2f02395a7910 Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 2 Mar 2024 13:31:57 +0100 Subject: [PATCH 52/56] minor refactoring --- types/type.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/type.ts b/types/type.ts index 93e11cd1..339b194d 100644 --- a/types/type.ts +++ b/types/type.ts @@ -259,7 +259,7 @@ export class Type extends ExtensibleFunction { return Runtime.castValue(this, VOID, context, context_location, origin); } - static #current_constructor:globalThis.Function; + static #current_constructor:globalThis.Function|null; public static isConstructing(value:object) { return value.constructor == this.#current_constructor; @@ -329,7 +329,7 @@ export class Type extends ExtensibleFunction { public newJSInstance(is_constructor = true, args?:any[], propertyInitializer?:{[INIT_PROPS]:(instance:any)=>void}) { // create new instance - TODO 'this' as last constructor argument still required? - Type.#current_constructor = this.interface_config?.class; + Type.#current_constructor = this.interface_config?.class??null; const instance = (this.interface_config?.class ? Reflect.construct(Type.#current_constructor, is_constructor?[...args]:(propertyInitializer ? [propertyInitializer] : [])) : {[DX_TYPE]: this}); Type.#current_constructor = null; return instance; From 471ab4e08e244cb0ee8c09c862ecade49f353d8e Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 2 Mar 2024 21:54:58 +0100 Subject: [PATCH 53/56] minor refactoring --- js_adapter/decorators.ts | 1 + js_adapter/js_class_adapter.ts | 12 ------------ runtime/js_interface.ts | 6 +++--- runtime/pointers.ts | 2 +- types/type.ts | 2 +- 5 files changed, 6 insertions(+), 17 deletions(-) diff --git a/js_adapter/decorators.ts b/js_adapter/decorators.ts index aa2816ef..14842015 100644 --- a/js_adapter/decorators.ts +++ b/js_adapter/decorators.ts @@ -67,6 +67,7 @@ export function endpoint(value: Class|target_clause|endpoint_name, context?: Cla }) } + /** * Sets a class as the entrypoint for the current endpoint */ diff --git a/js_adapter/js_class_adapter.ts b/js_adapter/js_class_adapter.ts index 21869017..a11a4a0d 100644 --- a/js_adapter/js_class_adapter.ts +++ b/js_adapter/js_class_adapter.ts @@ -148,18 +148,6 @@ export class Decorators { } } - /** @meta(index?:number): declare index of meta parameter (before method), or inline parameter decorator */ - static meta(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[string?] = []) { - - if (kind == "method") { - setMetadata(Decorators.META_INDEX, params[0] ?? -1); - } - - // invalid decorator call - else logger.error("@meta can only be used for methods"); - } - - /** @sign(sign?:boolean): sign outgoing DATEX requests (default:true) */ static sign(value:any, name:context_name, kind:context_kind, is_static:boolean, is_private:boolean, setMetadata:context_meta_setter, getMetadata:context_meta_getter, params:[boolean?] = []) { setMetadata(Decorators.SIGN, params[0]) diff --git a/runtime/js_interface.ts b/runtime/js_interface.ts index 7011ab5f..f9e124d9 100644 --- a/runtime/js_interface.ts +++ b/runtime/js_interface.ts @@ -292,13 +292,13 @@ export class JSInterface { } // js class -> - static getClassDatexType(class_constructor:Class):Type { + static getClassDatexType(class_constructor:Class):Type|undefined { let config:js_interface_configuration; // get directly from class - if (config = this.configurations_by_class.get(class_constructor)) return config.__type; + if ((config = this.configurations_by_class.get(class_constructor))) return config.__type; // get from prototype of class - if (config = this.configurations_by_class.get(Object.getPrototypeOf(class_constructor))) return config.__type; + if ((config = this.configurations_by_class.get(Object.getPrototypeOf(class_constructor)))) return config.__type; // check full prototype chain (should not happen normally, unnessary to loop through every time) // for (let [_class, config] of this.configurations_by_class) { diff --git a/runtime/pointers.ts b/runtime/pointers.ts index 60bbd88a..ea7ae1c6 100644 --- a/runtime/pointers.ts +++ b/runtime/pointers.ts @@ -802,7 +802,7 @@ type _Proxy$ = _Proxy$Function & } // normal object - : {readonly [K in keyof T]: RefLike} // always map properties to pointer property references + : {[K in keyof T]: RefLike} // always map properties to pointer property references ) type _PropertyProxy$ = _Proxy$Function & diff --git a/types/type.ts b/types/type.ts index 339b194d..74d51703 100644 --- a/types/type.ts +++ b/types/type.ts @@ -880,7 +880,7 @@ export class Type extends ExtensibleFunction { if (_forClass == Negation) return >Type.std.Negation; - let custom_type = JSInterface.getClassDatexType(_forClass); + const custom_type = JSInterface.getClassDatexType(_forClass); if (!custom_type) { From 197ced10e1c06b29ac5eed189e8b06dae0e7e00d Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 2 Mar 2024 23:45:43 +0100 Subject: [PATCH 54/56] fix decorators --- docs/manual/15 Storage Collections.md | 18 +++++++++--------- network/blockchain_adapter.ts | 10 +++++----- tests/sql-storage.test.ts | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/manual/15 Storage Collections.md b/docs/manual/15 Storage Collections.md index 06dd3d2c..7e44488d 100644 --- a/docs/manual/15 Storage Collections.md +++ b/docs/manual/15 Storage Collections.md @@ -60,9 +60,9 @@ import { StorageSet } from "datex-core-legacy/types/storage-set.ts"; import { Time } from "unyt_core/datex_all.ts"; @sync class User { - @property(string) declare name: string - @property(number) declare age: number - @property(Time) declare created: Time + @property(string) name!: string + @property(number) age!: number + @property(Time) created!: Time } const users = new StorageSet(); @@ -181,13 +181,13 @@ Example: import { ComputedProperty } from "datex-core-legacy/storage/storage.ts"; @sync class Location { - @property(number) declare lat: number - @property(number) declare lon: number + @property(number) lat!: number + @property(number) lon!: number } @sync class User { - @property(string) declare name: string - @property(Location) declare location: Location + @property(string) name!: string + @property(Location) location!: Location } @@ -226,8 +226,8 @@ Calculates the sum of multiple properties or literal values Example: ```ts @sync class TodoItem { - @property(number) declare completedTaskCount: number - @property(number) declare openTaskCount: number + @property(number) completedTaskCount!: number + @property(number) openTaskCount!: number } const todoItems = new StorageSet() diff --git a/network/blockchain_adapter.ts b/network/blockchain_adapter.ts index 5a194ab7..8c76e313 100644 --- a/network/blockchain_adapter.ts +++ b/network/blockchain_adapter.ts @@ -66,11 +66,11 @@ export type BCData = @sync export class BCEntry { - @property declare index: entry_index - @property declare type:T - @property declare data:BCData - @property declare creator?:Endpoint - @property declare signature?:ArrayBuffer + @property index!: entry_index + @property type!:T + @property data!:BCData + @property creator?:Endpoint + @property signature?:ArrayBuffer constructor(data?: { index: entry_index, diff --git a/tests/sql-storage.test.ts b/tests/sql-storage.test.ts index 9a42853a..165d7f1f 100644 --- a/tests/sql-storage.test.ts +++ b/tests/sql-storage.test.ts @@ -27,23 +27,23 @@ Datex.Storage.addLocation(sqlStorage, { }) @sync class Position { - @property declare x: number - @property declare y: number + @property x!: number + @property y!: number } @sync class Player { - @property declare name: string + @property name!: string @property @type('text(20)') declare username: string - @property declare color: bigint - @property declare pos: Position + @property color!: bigint + @property pos!: Position } @sync class ScoreboardEntry { - @property declare player: Player - @property declare score: number + @property player!: Player + @property score!: number } From 803ef773a37b91358ec3dfd3d23eaff1cc8d6a84 Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 2 Mar 2024 23:53:39 +0100 Subject: [PATCH 55/56] fix extension checks --- runtime/runtime.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtime/runtime.ts b/runtime/runtime.ts index 64cc3da3..e39868c4 100644 --- a/runtime/runtime.ts +++ b/runtime/runtime.ts @@ -625,12 +625,12 @@ export class Runtime { } // js module import - if (!raw && (url_string.endsWith("js") || url_string.endsWith("ts") || url_string.endsWith("tsx") || url_string.endsWith("jsx"))) { + if (!raw && (url_string.endsWith(".js") || url_string.endsWith(".ts") || url_string.endsWith(".tsx") || url_string.endsWith(".jsx"))) { doFetch = false; // no body fetch required, can directly import() module overrideContentType = "application/javascript" } // potential js module as dxb/dx: fetch headers first and check content type - else if (!raw && potentialDatexAsJsModule && (url_string.endsWith("dx") || url_string.endsWith("dxb"))) { + else if (!raw && potentialDatexAsJsModule && (url_string.endsWith(".dx") || url_string.endsWith(".dxb"))) { try { response = await fetch(url, {method: 'HEAD', cache: 'no-store'}); const type = response.headers.get('content-type'); From 43d6b6b3cc6b24d63271be9eeedf5ccba6aa3282 Mon Sep 17 00:00:00 2001 From: benStre Date: Sat, 2 Mar 2024 23:58:26 +0100 Subject: [PATCH 56/56] remove debug log --- types/time.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/types/time.ts b/types/time.ts index b0f2994f..f6210b39 100644 --- a/types/time.ts +++ b/types/time.ts @@ -68,8 +68,6 @@ export class Time extends Date { if (time.hasBaseUnit('s')) { this.setTime(this.getTime()+(time.value*1000)) - console.log(this.getTime(), time.value*1000, this.getTime()+(time.value*1000)) - } else if (time.hasBaseUnit('Cmo')) { this.setMonth(this.getMonth()+time.value);