Skip to content

Commit

Permalink
feat: introduce Link DB API (DAP-4685)
Browse files Browse the repository at this point in the history
  • Loading branch information
alsakhaev committed Jul 19, 2024
1 parent e0b5d90 commit f445970
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 22 deletions.
13 changes: 13 additions & 0 deletions libs/engine/src/app/common/memoize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type MemoizedFunction<T, R> = (arg: T) => R

export const memoize =
<T, R>(fn: MemoizedFunction<T, R>, map = new Map<T, R | MemoizedFunction<T, R>>()) =>
(arg: T): R | MemoizedFunction<T, R> => {
const inCache = map.has(arg)

if (!inCache) {
map.set(arg, fn(arg))
}

return map.get(arg) as R | MemoizedFunction<T, R>
}
2 changes: 1 addition & 1 deletion libs/engine/src/app/common/transferable-context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IContextNode } from "@mweb/core"

export interface TransferableContext {
namespace: string | null
namespace: string
type: string
id: string | null
parsed: any
Expand Down
81 changes: 61 additions & 20 deletions libs/engine/src/app/components/context-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ContextTree children={ContextHandler} />
Expand Down Expand Up @@ -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()

Expand All @@ -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,
Expand Down Expand Up @@ -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
})),
Expand All @@ -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 =
Expand Down
23 changes: 23 additions & 0 deletions libs/engine/src/app/services/link-db/link-db.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Value } from '../social-db/social-db.service'

export type LinkIndexRules = {
namespace?: boolean
type?: boolean
parsed: Record<string, boolean>
parent?: LinkIndexRules
}

export type IndexedContext = {
namespace?: string
type?: string
parsed?: Record<string, Value>
parent?: IndexedContext
}

export type IndexObject = {
appId: string
mutationId: string
context: IndexedContext
}

export type LinkedDataByAccount = { [accountId: string]: any }
125 changes: 125 additions & 0 deletions libs/engine/src/app/services/link-db/link-db.service.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<LinkedDataByAccount> {
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
}
}
4 changes: 3 additions & 1 deletion libs/engine/src/app/services/social-db/social-db.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions libs/engine/src/engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,6 +27,7 @@ export type EngineConfig = {
export class Engine {
#selector: WalletSelector

linkDbService: LinkDbService
mutationService: MutationService
applicationService: ApplicationService
userLinkService: UserLinkSerivce
Expand All @@ -38,13 +40,16 @@ 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)
const mutationRepository = new MutationRepository(socialDb, localDb)
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)
Expand Down

0 comments on commit f445970

Please sign in to comment.