From f445970bd4391d59296552480685eb7ac492b3ca Mon Sep 17 00:00:00 2001 From: Alexander Sakhaev Date: Fri, 19 Jul 2024 14:13:47 +0300 Subject: [PATCH] feat: introduce Link DB API (DAP-4685) --- libs/engine/src/app/common/memoize.ts | 13 ++ .../src/app/common/transferable-context.ts | 2 +- .../src/app/components/context-manager.tsx | 81 +++++++++--- .../app/services/link-db/link-db.entity.ts | 23 ++++ .../app/services/link-db/link-db.service.ts | 125 ++++++++++++++++++ .../services/social-db/social-db.service.ts | 4 +- libs/engine/src/engine.tsx | 5 + 7 files changed, 231 insertions(+), 22 deletions(-) create mode 100644 libs/engine/src/app/common/memoize.ts create mode 100644 libs/engine/src/app/services/link-db/link-db.entity.ts create mode 100644 libs/engine/src/app/services/link-db/link-db.service.ts diff --git a/libs/engine/src/app/common/memoize.ts b/libs/engine/src/app/common/memoize.ts new file mode 100644 index 00000000..9dc0c23e --- /dev/null +++ b/libs/engine/src/app/common/memoize.ts @@ -0,0 +1,13 @@ +type MemoizedFunction = (arg: T) => R + +export const memoize = + (fn: MemoizedFunction, map = new Map>()) => + (arg: T): R | MemoizedFunction => { + const inCache = map.has(arg) + + if (!inCache) { + map.set(arg, fn(arg)) + } + + return map.get(arg) as R | MemoizedFunction + } diff --git a/libs/engine/src/app/common/transferable-context.ts b/libs/engine/src/app/common/transferable-context.ts index c89d3cdb..27381ccb 100644 --- a/libs/engine/src/app/common/transferable-context.ts +++ b/libs/engine/src/app/common/transferable-context.ts @@ -1,7 +1,7 @@ import { IContextNode } from "@mweb/core" export interface TransferableContext { - namespace: string | null + namespace: string type: string id: string | null parsed: any diff --git a/libs/engine/src/app/components/context-manager.tsx b/libs/engine/src/app/components/context-manager.tsx index 13ff3752..b400776a 100644 --- a/libs/engine/src/app/components/context-manager.tsx +++ b/libs/engine/src/app/components/context-manager.tsx @@ -15,6 +15,8 @@ import { useModal } from '../contexts/modal-context' import { useMutableWeb } from '../contexts/mutable-web-context' import { BuiltInLayoutManagers } from '../../constants' import { TargetService } from '../services/target/target.service' +import { LinkedDataByAccount, LinkIndexRules } from '../services/link-db/link-db.entity' +import { memoize } from '../common/memoize' export const ContextManager: FC = () => { return @@ -114,7 +116,7 @@ const InsPointHandler: FC<{ onAttachContextRef, }) => { const { redirectMap, isDevServerLoading } = useEngine() - const { config, engine } = useMutableWeb() + const { config, engine, selectedMutation } = useMutableWeb() const { components } = usePortalFilter(context, insPointName) // ToDo: extract to the separate AppManager component const { notify } = useModal() @@ -130,6 +132,60 @@ const InsPointHandler: FC<{ [context, insPointName] ) + // These handlers are memoized to prevent unnecessary rerenders + const handleGetLinkDataCurry = useCallback( + memoize( + (appId: AppId) => + (ctx: TransferableContext, accountIds?: string[] | string, indexRules?: LinkIndexRules) => { + if (!selectedMutation) throw new Error('No selected mutation') + return engine.linkDbService.get(selectedMutation.id, appId, ctx, accountIds, indexRules) + } + ), + [engine, selectedMutation] + ) + + const handleSetLinkDataCurry = useCallback( + memoize( + (appId: AppId) => + ( + ctx: TransferableContext, + dataByAccount: LinkedDataByAccount, + indexRules: LinkIndexRules + ) => { + if (!selectedMutation) throw new Error('No selected mutation') + return engine.linkDbService.set( + selectedMutation.id, + appId, + ctx, + dataByAccount, + indexRules + ) + } + ), + [engine, selectedMutation] + ) + + // prevents blinking + if (isDevServerLoading) { + return null + } + + const layoutManagerId = bosLayoutManager + ? config.layoutManagers[bosLayoutManager as keyof BuiltInLayoutManagers] ?? bosLayoutManager + : config.layoutManagers.horizontal + + // Don't render layout manager if there are no components + // It improves performance + if ( + components.length === 0 && + !allUserLinks.some((link) => link.insertionPoint === insPointName) && + layoutManagerId !== config.layoutManagers.ear // ToDo: hardcode + ) { + return null + } + + // ToDo: extract App specific links to the separate AppManager component + const props = { // ToDo: unify context forwarding context: transferableContext, @@ -172,6 +228,10 @@ const InsPointHandler: FC<{ authorId: link.authorId, }, notify, + linkDb: { + get: handleGetLinkDataCurry(link.appId), + set: handleSetLinkDataCurry(link.appId), + }, }, // ToDo: add props isSuitable: link.insertionPoint === insPointName, // ToDo: LM know about widgets from other LM })), @@ -191,25 +251,6 @@ const InsPointHandler: FC<{ notify, } - // prevents blinking - if (isDevServerLoading) { - return null - } - - const layoutManagerId = bosLayoutManager - ? config.layoutManagers[bosLayoutManager as keyof BuiltInLayoutManagers] ?? bosLayoutManager - : config.layoutManagers.horizontal - - // Don't render layout manager if there are no components - // It improves performance - if ( - components.length === 0 && - !allUserLinks.some((link) => link.insertionPoint === insPointName) && - layoutManagerId !== config.layoutManagers.ear // ToDo: hardcode - ) { - return null - } - // ToDo: hardcode. The ear should be positioned relative to the contexts. // Inside of BOS-components impossible to set :host styles const shadowDomHostStyles: React.CSSProperties | undefined = diff --git a/libs/engine/src/app/services/link-db/link-db.entity.ts b/libs/engine/src/app/services/link-db/link-db.entity.ts new file mode 100644 index 00000000..13dccb7a --- /dev/null +++ b/libs/engine/src/app/services/link-db/link-db.entity.ts @@ -0,0 +1,23 @@ +import { Value } from '../social-db/social-db.service' + +export type LinkIndexRules = { + namespace?: boolean + type?: boolean + parsed: Record + parent?: LinkIndexRules +} + +export type IndexedContext = { + namespace?: string + type?: string + parsed?: Record + parent?: IndexedContext +} + +export type IndexObject = { + appId: string + mutationId: string + context: IndexedContext +} + +export type LinkedDataByAccount = { [accountId: string]: any } diff --git a/libs/engine/src/app/services/link-db/link-db.service.ts b/libs/engine/src/app/services/link-db/link-db.service.ts new file mode 100644 index 00000000..a21dcecd --- /dev/null +++ b/libs/engine/src/app/services/link-db/link-db.service.ts @@ -0,0 +1,125 @@ +import serializeToDeterministicJson from 'json-stringify-deterministic' + +import { SocialDbService } from '../social-db/social-db.service' +import { TransferableContext } from '../../common/transferable-context' +import { AppId } from '../application/application.entity' +import { MutationId } from '../mutation/mutation.entity' +import { UserLinkRepository } from '../user-link/user-link.repository' +import { LinkIndexRules, IndexObject, LinkedDataByAccount } from './link-db.entity' + +const DefaultIndexRules: LinkIndexRules = { + namespace: true, + type: true, + parsed: { id: true }, +} + +const ProjectIdKey = 'dapplets.near' +const SettingsKey = 'settings' +const ContextLinkKey = 'ctxlink' +const WildcardKey = '*' +const KeyDelimiter = '/' +const DataKey = 'data' + +export class LinkDbService { + constructor(private _socialDb: SocialDbService) {} + + async set( + mutationId: MutationId, + appId: AppId, + context: TransferableContext, // ToDo: replace with IContextNode? + dataByAccount: LinkedDataByAccount, + indexRules: LinkIndexRules = DefaultIndexRules + ): Promise { + const accounts = Object.keys(dataByAccount) + + // ToDo: implement multiple accounts + if (accounts.length !== 1) { + throw new Error('Only one account can be written at a time') + } + + const [accountId] = accounts + + const indexObject = LinkDbService._buildLinkIndex(mutationId, appId, indexRules, context) + const index = UserLinkRepository._hashObject(indexObject) // ToDo: the dependency is not injected + + const keys = [accountId, SettingsKey, ProjectIdKey, ContextLinkKey, index] + + const dataToStore = { + [DataKey]: serializeToDeterministicJson(dataByAccount[accountId]), + } + + await this._socialDb.set(SocialDbService.buildNestedData(keys, dataToStore)) + } + + async get( + mutationId: MutationId, + appId: AppId, + context: TransferableContext, + accountIds: string[] | string = [WildcardKey], // from any user by default + indexRules: LinkIndexRules = DefaultIndexRules // use context id as index by default + ): Promise { + const indexObject = LinkDbService._buildLinkIndex(mutationId, appId, indexRules, context) + const index = UserLinkRepository._hashObject(indexObject) // ToDo: the dependency is not injected + + accountIds = Array.isArray(accountIds) ? accountIds : [accountIds] + + const keysArr = accountIds.map((accountId) => [ + accountId, + SettingsKey, + ProjectIdKey, + ContextLinkKey, + index, + DataKey, + ]) + + // ToDo: too much data will be retrieved here, becuase it created by users + + // ToDo: batch requests + const resp = await this._socialDb.get(keysArr.map((keys) => keys.join(KeyDelimiter))) + + const links = SocialDbService.splitObjectByDepth(resp, 6) // 6 is a number of keys in keysArr + + const dataByAuthor = Object.fromEntries( + Object.entries(links).map(([key, json]) => { + const [authorId] = key.split(KeyDelimiter) + const data = json ? JSON.parse(json as string) : undefined + return [authorId, data] + }) + ) + + return dataByAuthor + } + + static _buildLinkIndex( + mutationId: MutationId, + appId: AppId, + indexRules: LinkIndexRules, + context: TransferableContext + ): IndexObject { + // MutationId is a part of the index. + // It means that a data of the same application is different in different mutations + + return { + mutationId, + appId, + context: LinkDbService._buildIndexedContextValues(indexRules, context), + } + } + + static _buildIndexedContextValues(indexes: any, values: any): any { + const out: any = {} + + for (const prop in indexes) { + if (!indexes[prop]) continue + + // ToDo: will not work with arrays + if (typeof values[prop] === 'object') { + out[prop] = LinkDbService._buildIndexedContextValues(indexes[prop], values[prop]) + } else { + out[prop] = values[prop] + } + } + + return out + } +} diff --git a/libs/engine/src/app/services/social-db/social-db.service.ts b/libs/engine/src/app/services/social-db/social-db.service.ts index 39c24fea..7b9f189e 100644 --- a/libs/engine/src/app/services/social-db/social-db.service.ts +++ b/libs/engine/src/app/services/social-db/social-db.service.ts @@ -139,7 +139,9 @@ export class SocialDbService { } if (accountId !== signedAccountId) { - throw new Error('Only the owner can update the account') + throw new Error( + `Only the owner can update the account. Got ${accountId}, expected ${signedAccountId}` + ) } const accountStorage = await this._getAccountStorage(signedAccountId) diff --git a/libs/engine/src/engine.tsx b/libs/engine/src/engine.tsx index c9ebe149..33d72540 100644 --- a/libs/engine/src/engine.tsx +++ b/libs/engine/src/engine.tsx @@ -13,6 +13,7 @@ import { MutationService } from './app/services/mutation/mutation.service' import { ApplicationService } from './app/services/application/application.service' import { UserLinkSerivce } from './app/services/user-link/user-link.service' import { ParserConfigService } from './app/services/parser-config/parser-config.service' +import { LinkDbService } from './app/services/link-db/link-db.service' export type EngineConfig = { networkId: string @@ -26,6 +27,7 @@ export type EngineConfig = { export class Engine { #selector: WalletSelector + linkDbService: LinkDbService mutationService: MutationService applicationService: ApplicationService userLinkService: UserLinkSerivce @@ -38,6 +40,7 @@ export class Engine { this.#selector = this.config.selector const nearConfig = getNearConfig(this.config.networkId) + const localDb = new LocalDbService(this.config.storage) const nearSigner = new NearSigner(this.#selector, localDb, nearConfig) const socialDb = new SocialDbService(nearSigner, nearConfig.contractName) @@ -45,6 +48,8 @@ export class Engine { const applicationRepository = new ApplicationRepository(socialDb, localDb) const userLinkRepository = new UserLinkRepository(socialDb, nearSigner) const parserConfigRepository = new ParserConfigRepository(socialDb) + + this.linkDbService = new LinkDbService(socialDb) this.mutationService = new MutationService(mutationRepository, nearConfig) this.applicationService = new ApplicationService(applicationRepository) this.userLinkService = new UserLinkSerivce(userLinkRepository, this.applicationService)