Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions packages/repluggable-core/src/API.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Redux from 'redux'
import { ThrottledStore } from './throttledStore'
import { INTERNAL_DONT_USE_SHELL_GET_APP_HOST } from './__internal'
import { CustomCreateExtensionSlot } from './extensionSlot'
import { ThrottledStore } from './throttledStore'

export interface AnySlotKey {
readonly name: string
Expand Down Expand Up @@ -77,6 +77,11 @@ export interface EntryPoint {
* @return {SlotKey<any>[]} API keys to wait for implementation
*/
getDependencyAPIs?(): SlotKey<any>[]
/**
* Define which API keys (a.k.a. contracts) are required for implementation but optional for this entry point to be executed
* @return {SlotKey<any>[]} API keys that may be used but don't block loading
*/
getColdDependencyAPIs?(): SlotKey<any>[]
/**
* Define which API keys (a.k.a. contracts) this entry point is going to implement and contribute
* @return {SlotKey<any>[]} API keys that will be contributed
Expand Down Expand Up @@ -331,7 +336,7 @@ interface AppHostPlugins {
}
}

export type {CustomCreateExtensionSlot}
export type { CustomCreateExtensionSlot }

export interface AppHostOptions {
readonly logger?: HostLogger
Expand Down Expand Up @@ -406,6 +411,15 @@ export interface Shell extends Pick<AppHost, Exclude<keyof AppHost, 'getStore' |
* @return {*} {boolean}
*/
canUseAPIs(): boolean
/**
* Get a lazy accessor for a cold dependency API.
* The API is only resolved when called, allowing safe usage during attach phase.
*
* @template TAPI
* @param {SlotKey<TAPI>} key API Key for the cold dependency
* @return {Lazy<TAPI>} Lazy wrapper that resolves the API on access
*/
getColdAPI<TAPI>(key: SlotKey<TAPI>): Lazy<TAPI>
/**
* Is store ready to be requested
*
Expand Down Expand Up @@ -525,6 +539,7 @@ export interface Shell extends Pick<AppHost, Exclude<keyof AppHost, 'getStore' |
export interface PrivateShell extends Shell {
readonly entryPoint: EntryPoint
setDependencyAPIs(APIs: AnySlotKey[]): void
setColdDependencyAPIs(APIs: AnySlotKey[]): void
setLifecycleState(enableStore: boolean, enableAPIs: boolean, initCompleted: boolean): void
getBoundaryAspects(): ShellBoundaryAspect[]
getHostOptions(): AppHostOptions
Expand All @@ -541,6 +556,7 @@ export interface EntryPointInterceptor {
interceptName?(innerName: string): string
interceptTags?(innerTags?: EntryPointTags): EntryPointTags
interceptGetDependencyAPIs?(innerGetDependencyAPIs?: EntryPoint['getDependencyAPIs']): EntryPoint['getDependencyAPIs']
interceptGetColdDependencyAPIs?(innerGetColdDependencyAPIs?: EntryPoint['getColdDependencyAPIs']): EntryPoint['getColdDependencyAPIs']
interceptDeclareAPIs?(innerDeclareAPIs?: EntryPoint['declareAPIs']): EntryPoint['declareAPIs']
interceptAttach?(innerAttach?: EntryPoint['attach']): EntryPoint['attach']
interceptDetach?(innerDetach?: EntryPoint['detach']): EntryPoint['detach']
Expand Down
68 changes: 62 additions & 6 deletions packages/repluggable-core/src/appHost.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import _ from 'lodash'
import { AnyAction, Store } from 'redux'
import { INTERNAL_DONT_USE_SHELL_GET_APP_HOST } from './__internal'
import {
AnyEntryPoint,
AnyFunction,
Expand Down Expand Up @@ -34,7 +35,7 @@ import {
UnsubscribeFromDeclarationsChanged
} from './API'
import { AppHostAPI, AppHostServicesProvider, createAppHostServicesEntryPoint } from './appHostServices'
import { declaredAPIs, dependentAPIs } from './appHostUtils'
import { coldDependentAPIs, declaredAPIs, dependentAPIs } from './appHostUtils'
import { AnyExtensionSlot, createCustomExtensionSlot, createExtensionSlot } from './extensionSlot'
import { InstalledShellsActions, InstalledShellsSelectors, ShellToggleSet } from './installedShellsState'
import { IterableWeakMap } from './IterableWeakMap'
Expand All @@ -51,7 +52,6 @@ import {
ThrottledStore,
updateThrottledStore
} from './throttledStore'
import { INTERNAL_DONT_USE_SHELL_GET_APP_HOST } from './__internal'

function isMultiArray<T>(v: T[] | T[][]): v is T[][] {
return _.every(v, _.isArray)
Expand Down Expand Up @@ -427,11 +427,49 @@ miss: ${memoizedWithMissHit.miss}
}
}

function buildApiToEntryPointMap(entryPoints: EntryPoint[]): Map<string, EntryPoint> {
const map = new Map<string, EntryPoint>()
for (const ep of entryPoints) {
for (const api of declaredAPIs(ep)) {
map.set(slotKeyToName(api), ep)
}
}
return map
}

function getEffectiveBlockingDeps(
entryPoint: EntryPoint,
apiToEntryPoint: Map<string, EntryPoint>,
visited = new Set<string>()
): AnySlotKey[] {
if (visited.has(entryPoint.name)) {
return []
}
visited.add(entryPoint.name)

const regularDeps = dependentAPIs(entryPoint)
const result = [...regularDeps]

for (const dep of regularDeps) {
const provider = apiToEntryPoint.get(slotKeyToName(dep))
if (provider) {
result.push(...coldDependentAPIs(provider))
result.push(...getEffectiveBlockingDeps(provider, apiToEntryPoint, visited))
}
}
return result
}

function executeInstallShell(entryPoints: EntryPoint[]): void {
const allEntryPoints = [...addedShells.values()]
.map(s => s.entryPoint)
.concat(unReadyEntryPointsStore.get(), entryPoints)
const apiToEntryPoint = buildApiToEntryPointMap(allEntryPoints)

const [readyEntryPoints, currentUnReadyEntryPoints] = _.partition(entryPoints, entryPoint => {
const dependencies = entryPoint.getDependencyAPIs && entryPoint.getDependencyAPIs()
const effectiveDeps = getEffectiveBlockingDeps(entryPoint, apiToEntryPoint)
return _.every(
dependencies,
effectiveDeps,
k =>
readyAPIs.has(getOwnSlotKey(k)) ||
(options.experimentalCyclicMode && isAllAPIDependenciesAreReadyOrPending(k, entryPoints))
Expand All @@ -455,8 +493,11 @@ miss: ${memoizedWithMissHit.miss}
invokeEntryPointPhase(
'getDependencyAPIs',
shells,
f => f.entryPoint.getDependencyAPIs && f.setDependencyAPIs(f.entryPoint.getDependencyAPIs()),
f => !!f.entryPoint.getDependencyAPIs
f => {
f.entryPoint.getDependencyAPIs && f.setDependencyAPIs(f.entryPoint.getDependencyAPIs())
f.entryPoint.getColdDependencyAPIs && f.setColdDependencyAPIs(f.entryPoint.getColdDependencyAPIs())
},
f => !!f.entryPoint.getDependencyAPIs || !!f.entryPoint.getColdDependencyAPIs
)

invokeEntryPointPhase(
Expand Down Expand Up @@ -932,6 +973,7 @@ miss: ${memoizedWithMissHit.miss}
let APIsEnabled = false
let wasInitCompleted = false
let dependencyAPIs: Set<AnySlotKey> = new Set()
let coldDependencyAPIs: Set<AnySlotKey> = new Set()
let nextObservableId = 1
const boundaryAspects: ShellBoundaryAspect[] = []

Expand Down Expand Up @@ -991,6 +1033,10 @@ miss: ${memoizedWithMissHit.miss}
dependencyAPIs = new Set(APIs)
},

setColdDependencyAPIs(APIs: AnySlotKey[]): void {
coldDependencyAPIs = new Set(APIs)
},

canUseAPIs(): boolean {
return APIsEnabled
},
Expand Down Expand Up @@ -1037,6 +1083,12 @@ miss: ${memoizedWithMissHit.miss}
if (dependencyAPIs.has(key) || isOwnContributedAPI(key)) {
return host.getAPI(key)
}
if (coldDependencyAPIs.has(key)) {
if (!wasInitCompleted) {
throw new Error(`API '${slotKeyToName(key)}' is a cold dependency and cannot be accessed during initialization`)
}
return host.getAPI(key)
}
throw new Error(
`API '${slotKeyToName(key)}' is not declared as dependency by entry point '${
entryPoint.name
Expand All @@ -1048,6 +1100,10 @@ miss: ${memoizedWithMissHit.miss}
return (dependencyAPIs.has(key) || isOwnContributedAPI(key)) && host.hasAPI(key)
},

getColdAPI<TAPI>(key: SlotKey<TAPI>): Lazy<TAPI> {
return shell.lazyEvaluator(() => shell.getAPI(key))
},

contributeAPI<TAPI>(key: SlotKey<TAPI>, factory: () => TAPI, apiOptions?: ContributeAPIOptions<TAPI>): TAPI {
host.log.log('verbose', `Contributing API ${slotKeyToName(key)}.`)

Expand Down
3 changes: 3 additions & 0 deletions packages/repluggable-core/src/appHostUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import _ from 'lodash'
export const dependentAPIs = (entryPoint: AnyEntryPoint): AnySlotKey[] => {
return _.chain(entryPoint).invoke('getDependencyAPIs').defaultTo([]).value()
}
export const coldDependentAPIs = (entryPoint: AnyEntryPoint): AnySlotKey[] => {
return _.chain(entryPoint).invoke('getColdDependencyAPIs').defaultTo([]).value()
}

export const declaredAPIs = (entryPoint: AnyEntryPoint): AnySlotKey[] => {
return _.chain(entryPoint).invoke('declareAPIs').defaultTo([]).value()
Expand Down
Loading
Loading