repluggable-core 1.302.76 → 1.302.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/debug/src/API.d.ts +8 -1
  2. package/dist/debug/src/extensionSlot.d.ts +13 -2
  3. package/dist/es/src/API.d.ts +8 -1
  4. package/dist/es/src/API.d.ts.map +1 -1
  5. package/dist/es/src/IterableWeakMap.d.ts.map +1 -1
  6. package/dist/es/src/IterableWeakMap.js.map +1 -1
  7. package/dist/es/src/appHost.d.ts.map +1 -1
  8. package/dist/es/src/appHost.js +11 -2
  9. package/dist/es/src/appHost.js.map +1 -1
  10. package/dist/es/src/extensionSlot.d.ts +13 -2
  11. package/dist/es/src/extensionSlot.d.ts.map +1 -1
  12. package/dist/es/src/extensionSlot.js +21 -9
  13. package/dist/es/src/extensionSlot.js.map +1 -1
  14. package/dist/es/src/index.d.ts.map +1 -1
  15. package/dist/es/src/index.js.map +1 -1
  16. package/dist/es/src/throttledStore.d.ts.map +1 -1
  17. package/dist/es/src/throttledStore.js.map +1 -1
  18. package/dist/es/test/appHost.spec.js +71 -0
  19. package/dist/es/test/appHost.spec.js.map +1 -1
  20. package/dist/es/test/createSignalItemsDataStructure.d.ts +16 -0
  21. package/dist/es/test/createSignalItemsDataStructure.d.ts.map +1 -0
  22. package/dist/es/test/createSignalItemsDataStructure.js +54 -0
  23. package/dist/es/test/createSignalItemsDataStructure.js.map +1 -0
  24. package/dist/es/test/extensionSlot.spec.d.ts +2 -0
  25. package/dist/es/test/extensionSlot.spec.d.ts.map +1 -0
  26. package/dist/es/test/extensionSlot.spec.js +136 -0
  27. package/dist/es/test/extensionSlot.spec.js.map +1 -0
  28. package/dist/es/testKit/index.d.ts +1 -0
  29. package/dist/es/testKit/index.d.ts.map +1 -1
  30. package/dist/es/testKit/index.js +1 -0
  31. package/dist/es/testKit/index.js.map +1 -1
  32. package/dist/src/API.d.ts +8 -1
  33. package/dist/src/API.d.ts.map +1 -1
  34. package/dist/src/IterableWeakMap.d.ts.map +1 -1
  35. package/dist/src/IterableWeakMap.js.map +1 -1
  36. package/dist/src/appHost.d.ts.map +1 -1
  37. package/dist/src/appHost.js +11 -2
  38. package/dist/src/appHost.js.map +1 -1
  39. package/dist/src/extensionSlot.d.ts +13 -2
  40. package/dist/src/extensionSlot.d.ts.map +1 -1
  41. package/dist/src/extensionSlot.js +21 -9
  42. package/dist/src/extensionSlot.js.map +1 -1
  43. package/dist/src/index.d.ts.map +1 -1
  44. package/dist/src/index.js.map +1 -1
  45. package/dist/src/throttledStore.d.ts.map +1 -1
  46. package/dist/src/throttledStore.js.map +1 -1
  47. package/dist/test/appHost.spec.js +71 -0
  48. package/dist/test/appHost.spec.js.map +1 -1
  49. package/dist/test/createSignalItemsDataStructure.d.ts +16 -0
  50. package/dist/test/createSignalItemsDataStructure.d.ts.map +1 -0
  51. package/dist/test/createSignalItemsDataStructure.js +58 -0
  52. package/dist/test/createSignalItemsDataStructure.js.map +1 -0
  53. package/dist/test/extensionSlot.spec.d.ts +2 -0
  54. package/dist/test/extensionSlot.spec.d.ts.map +1 -0
  55. package/dist/test/extensionSlot.spec.js +138 -0
  56. package/dist/test/extensionSlot.spec.js.map +1 -0
  57. package/dist/testKit/index.d.ts +1 -0
  58. package/dist/testKit/index.d.ts.map +1 -1
  59. package/dist/testKit/index.js +3 -1
  60. package/dist/testKit/index.js.map +1 -1
  61. package/package.json +3 -2
  62. package/src/API.ts +10 -4
  63. package/src/IterableWeakMap.ts +3 -5
  64. package/src/__internal.ts +1 -1
  65. package/src/appHost.ts +10 -2
  66. package/src/extensionSlot.ts +36 -10
  67. package/src/index.ts +2 -5
  68. package/src/throttledStore.ts +9 -15
  69. package/test/appHost.spec.ts +89 -0
  70. package/test/createSignalItemsDataStructure.ts +65 -0
  71. package/test/extensionSlot.spec.ts +176 -0
  72. package/testKit/index.ts +7 -7
package/src/API.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as Redux from 'redux'
2
2
  import { ThrottledStore } from './throttledStore'
3
3
  import { INTERNAL_DONT_USE_SHELL_GET_APP_HOST } from './__internal'
4
-
4
+ import { CustomCreateExtensionSlot } from './extensionSlot'
5
5
 
6
6
  export interface AnySlotKey {
7
7
  readonly name: string
@@ -333,6 +333,12 @@ export interface APILayer {
333
333
  name: string
334
334
  }
335
335
 
336
+ interface AppHostPlugins {
337
+ extensionSlot?: {
338
+ customCreateExtensionSlot: CustomCreateExtensionSlot
339
+ }
340
+ }
341
+
336
342
  export interface AppHostOptions {
337
343
  readonly logger?: HostLogger
338
344
  readonly monitoring: MonitoringOptions
@@ -343,6 +349,7 @@ export interface AppHostOptions {
343
349
  readonly enableReduxDevtoolsExtension?: boolean
344
350
  readonly experimentalCyclicMode?: boolean
345
351
  readonly shouldScopeReducers?: boolean
352
+ readonly plugins?: AppHostPlugins
346
353
  }
347
354
 
348
355
  export interface MemoizeMissHit {
@@ -526,9 +533,8 @@ export interface PrivateShell extends Shell {
526
533
  setDependencyAPIs(APIs: AnySlotKey[]): void
527
534
  setLifecycleState(enableStore: boolean, enableAPIs: boolean, initCompleted: boolean): void
528
535
  getBoundaryAspects(): ShellBoundaryAspect[]
529
- getHostOptions(): AppHostOptions,
530
- readonly [INTERNAL_DONT_USE_SHELL_GET_APP_HOST]: () => AppHost
531
-
536
+ getHostOptions(): AppHostOptions
537
+ [INTERNAL_DONT_USE_SHELL_GET_APP_HOST](): AppHost
532
538
  }
533
539
 
534
540
  export interface EntryPointsInfo {
@@ -81,7 +81,7 @@ export class IterableWeakMap<K extends object = object, V = any> implements Map<
81
81
 
82
82
  forEach(callbackfn: (value: V, key: K, map: this) => void, thisArg?: any): void {
83
83
  for (const [key, value] of this) {
84
- callbackfn(value, key, this )
84
+ callbackfn(value, key, this)
85
85
  }
86
86
  }
87
87
 
@@ -107,17 +107,15 @@ export class IterableWeakMap<K extends object = object, V = any> implements Map<
107
107
  return this[Symbol.iterator]() as unknown as ReturnType<Map<K, V>['entries']>
108
108
  }
109
109
 
110
- *keys(): ReturnType<Map<K, V>['keys']> {
110
+ *keys(): ReturnType<Map<K, V>['keys']> {
111
111
  for (const [key] of this) {
112
112
  yield key
113
113
  }
114
114
  }
115
115
 
116
- *values(): ReturnType<Map<K, V>['values']> {
116
+ *values(): ReturnType<Map<K, V>['values']> {
117
117
  for (const [_, value] of this) {
118
118
  yield value
119
119
  }
120
120
  }
121
121
  }
122
-
123
-
package/src/__internal.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * This symbol is only for internal use of repluggable mono-repo.
3
3
  */
4
- export const INTERNAL_DONT_USE_SHELL_GET_APP_HOST: unique symbol = Symbol.for('SHELL_GET_APP_HOST')
4
+ export const INTERNAL_DONT_USE_SHELL_GET_APP_HOST: unique symbol = Symbol.for('SHELL_GET_APP_HOST')
package/src/appHost.ts CHANGED
@@ -251,7 +251,10 @@ export function createAppHost(initialEntryPointsOrPackages: EntryPointOrPackage[
251
251
  const memoized = _.memoize(func, resolver)
252
252
 
253
253
  if (options.monitoring.debugMemoization) {
254
- Object.defineProperty(memoized, 'name', { value: `${func.name}_memoized`, writable: false })
254
+ Object.defineProperty(memoized, 'name', {
255
+ value: `${func.name}_memoized`,
256
+ writable: false
257
+ })
255
258
  }
256
259
 
257
260
  if (options.monitoring.disableMonitoring) {
@@ -560,7 +563,12 @@ miss: ${memoizedWithMissHit.miss}
560
563
  }
561
564
 
562
565
  function declareSlot<TItem>(key: SlotKey<TItem>, declaringShell?: Shell): ExtensionSlot<TItem> {
563
- const newSlot = registerSlotOrThrow(key, () => createExtensionSlot(key, host, declaringShell))
566
+ const newSlot = registerSlotOrThrow(key, () =>
567
+ createExtensionSlot(key, host, {
568
+ declaringShell,
569
+ customCreateExtensionSlot: options.plugins?.extensionSlot?.customCreateExtensionSlot
570
+ })
571
+ )
564
572
  return newSlot
565
573
  }
566
574
 
@@ -21,18 +21,44 @@ const alwaysTrue = () => true
21
21
 
22
22
  type Unsubscribe = () => void
23
23
 
24
+ interface ItemsDataStructure<T> {
25
+ get(): ExtensionItem<T>[]
26
+ add(item: ExtensionItem<T>): void
27
+ discardBy(predicate: (item: ExtensionItem<T>) => boolean): void
28
+ }
29
+
30
+ const itemsDataStructure = <T>(): ItemsDataStructure<T> => {
31
+ let items: ExtensionItem<T>[] = []
32
+ return {
33
+ get: () => items,
34
+ add: (item: ExtensionItem<T>) => {
35
+ items.push(item)
36
+ },
37
+ discardBy: (predicate: (item: ExtensionItem<T>) => boolean) => {
38
+ items = items.filter(predicate)
39
+ }
40
+ }
41
+ }
42
+
43
+ export type CustomCreateExtensionSlot = <T>() => ItemsDataStructure<T>
44
+
45
+ export interface CreateExtensionSlotOptions {
46
+ declaringShell?: Shell
47
+ customCreateExtensionSlot?: CustomCreateExtensionSlot
48
+ }
49
+
24
50
  export function createExtensionSlot<T>(
25
51
  key: SlotKey<T>,
26
52
  host: PrivateAppHost,
27
- declaringShell?: Shell
53
+ options?: CreateExtensionSlotOptions
28
54
  ): PrivateExtensionSlot<T> & AnyExtensionSlot {
29
- let items: ExtensionItem<T>[] = []
55
+ const items = options?.customCreateExtensionSlot ? options.customCreateExtensionSlot<T>() : itemsDataStructure<T>()
30
56
  let subscribers: (() => void)[] = []
31
57
  const slotUniqueId = _.uniqueId()
32
58
 
33
59
  return {
34
60
  host,
35
- declaringShell,
61
+ declaringShell: options?.declaringShell,
36
62
  name: key.name,
37
63
  contribute,
38
64
  getItems,
@@ -43,7 +69,7 @@ export function createExtensionSlot<T>(
43
69
  }
44
70
 
45
71
  function contribute(fromShell: Shell, item: T, condition?: ContributionPredicate): void {
46
- items.push({
72
+ items.add({
47
73
  shell: fromShell,
48
74
  contribution: item,
49
75
  condition: condition || alwaysTrue,
@@ -53,21 +79,21 @@ export function createExtensionSlot<T>(
53
79
  }
54
80
 
55
81
  function getItems(forceAll: boolean = false): ExtensionItem<T>[] {
56
- return forceAll ? items : items.filter(item => item.condition())
82
+ return forceAll ? items.get() : items.get().filter(item => item.condition())
57
83
  }
58
84
 
59
85
  function getSingleItem(): ExtensionItem<T> | undefined {
60
- return items.find(item => item.condition())
86
+ return items.get().find(item => item.condition())
61
87
  }
62
88
 
63
89
  function getItemByName(name: string): ExtensionItem<T> | undefined {
64
- return items.find(item => item.name === name && item.condition())
90
+ return items.get().find(item => item.name === name && item.condition())
65
91
  }
66
92
 
67
93
  function discardBy(predicate: ExtensionItemFilter<T>) {
68
- const originalContributionCount = items.length
69
- items = items.filter(v => !predicate(v))
70
- if (items.length !== originalContributionCount) {
94
+ const originalContributionCount = items.get().length
95
+ items.discardBy(v => !predicate(v))
96
+ if (items.get().length !== originalContributionCount) {
71
97
  subscribers.forEach(func => func())
72
98
  }
73
99
  }
package/src/index.ts CHANGED
@@ -26,13 +26,11 @@ export {
26
26
  ScopedStore
27
27
  } from './API'
28
28
 
29
-
30
-
31
29
  export { ConsoleHostLogger } from './loggers'
32
30
 
33
31
  export { createAppHost, makeLazyEntryPoint, mainViewSlotKey, stateSlotKey } from './appHost'
34
32
 
35
- export {AppHostServicesProvider} from './appHostServices'
33
+ export { AppHostServicesProvider } from './appHostServices'
36
34
 
37
35
  export { InstalledShellsSelectors, ShellToggleSet } from './installedShellsState'
38
36
 
@@ -42,5 +40,4 @@ export { interceptAnyObject } from './interceptAnyObject'
42
40
  export { monitorAPI } from './monitorAPI'
43
41
  export { hot } from './hot'
44
42
 
45
-
46
- export {INTERNAL_DONT_USE_SHELL_GET_APP_HOST} from './__internal'
43
+ export { INTERNAL_DONT_USE_SHELL_GET_APP_HOST } from './__internal'
@@ -16,18 +16,17 @@ interface AnyShellAction extends AnyAction {
16
16
  __shellName?: string
17
17
  }
18
18
 
19
-
20
- function createTimeOutPublisher ( notify: () => void) {
21
- let id : undefined | NodeJS.Timeout = undefined
19
+ function createTimeOutPublisher(notify: () => void) {
20
+ let id: undefined | NodeJS.Timeout = undefined
22
21
  return () => {
23
22
  if (id === undefined) {
24
23
  id = setTimeout(() => {
25
24
  id = undefined
26
25
  notify()
27
- },0)
26
+ }, 0)
28
27
  }
29
28
  return () => {
30
- if(id === undefined) {
29
+ if (id === undefined) {
31
30
  return
32
31
  }
33
32
  clearTimeout(id)
@@ -36,8 +35,8 @@ function createTimeOutPublisher ( notify: () => void) {
36
35
  }
37
36
  }
38
37
 
39
- function createAnimationFramePublisher ( notify: () => void) {
40
- let id : undefined | number = undefined
38
+ function createAnimationFramePublisher(notify: () => void) {
39
+ let id: undefined | number = undefined
41
40
  return () => {
42
41
  if (id === undefined) {
43
42
  id = requestAnimationFrame(() => {
@@ -46,7 +45,7 @@ function createAnimationFramePublisher ( notify: () => void) {
46
45
  })
47
46
  }
48
47
  return () => {
49
- if(id === undefined) {
48
+ if (id === undefined) {
50
49
  return
51
50
  }
52
51
  cancelAnimationFrame(id)
@@ -55,9 +54,6 @@ function createAnimationFramePublisher ( notify: () => void) {
55
54
  }
56
55
  }
57
56
 
58
-
59
-
60
-
61
57
  type Subscriber = () => void
62
58
 
63
59
  export interface StateContribution<TState = {}, TAction extends AnyAction = AnyAction> {
@@ -252,10 +248,8 @@ export const createThrottledStore = (
252
248
  notifyAll()
253
249
  }
254
250
 
255
-
256
-
257
-
258
- const notifyAllOnPublish = typeof window === 'undefined' ? createTimeOutPublisher(scheduledNotifyAll) : createAnimationFramePublisher(scheduledNotifyAll)
251
+ const notifyAllOnPublish =
252
+ typeof window === 'undefined' ? createTimeOutPublisher(scheduledNotifyAll) : createAnimationFramePublisher(scheduledNotifyAll)
259
253
 
260
254
  let cancelRender = _.noop
261
255
 
@@ -32,6 +32,7 @@ import {
32
32
  import { AppHostAPI, AppHostServicesEntryPointName, AppHostServicesProvider } from '../src/appHostServices'
33
33
  import { ConsoleHostLogger } from '../src/loggers'
34
34
  import { createCircularEntryPoints, createDirectCircularEntryPoints } from './appHost.mock'
35
+ import { createSignalItemsDataStructure } from './createSignalItemsDataStructure'
35
36
 
36
37
  const testHostOptions: AppHostOptions = {
37
38
  monitoring: { disableMonitoring: true }
@@ -1792,4 +1793,92 @@ If the API is intended to be public, it should be declared as "public: true" in
1792
1793
  )
1793
1794
  })
1794
1795
  })
1796
+
1797
+ describe('Custom Items Data Structure via AppHost Options (plugins)', () => {
1798
+ it('should use customCreateExtensionSlot from appHostOptions when contributing and removing items', async () => {
1799
+ interface SlotItem {
1800
+ value: string
1801
+ }
1802
+ const slotKey: SlotKey<SlotItem> = { name: 'host_options_signal_slot' }
1803
+
1804
+ interface SlotContributionAPI {
1805
+ contributeItem(fromShell: Shell, item: SlotItem): void
1806
+ getItems(): SlotItem[]
1807
+ }
1808
+ const SlotContributionAPIKey: SlotKey<SlotContributionAPI> = {
1809
+ name: 'SLOT_CONTRIBUTION_API'
1810
+ }
1811
+
1812
+ const itemsSpy = jest.fn()
1813
+
1814
+ const { createDataStructure, effect } = createSignalItemsDataStructure()
1815
+
1816
+ const slotOwnerEntryPoint: EntryPoint = {
1817
+ name: 'SLOT_OWNER_ENTRY_POINT',
1818
+ declareAPIs() {
1819
+ return [SlotContributionAPIKey]
1820
+ },
1821
+ attach(shell: Shell) {
1822
+ const slot = shell.declareSlot(slotKey)
1823
+ shell.contributeAPI(SlotContributionAPIKey, () => ({
1824
+ contributeItem(fromShell: Shell, item: SlotItem) {
1825
+ slot.contribute(fromShell, item)
1826
+ },
1827
+ getItems() {
1828
+ return slot.getItems().map(item => item.contribution)
1829
+ }
1830
+ }))
1831
+ }
1832
+ }
1833
+
1834
+ const ContributorEntryPoint: EntryPoint = {
1835
+ name: 'CONTRIBUTOR_ENTRY_POINT',
1836
+ getDependencyAPIs() {
1837
+ return [SlotContributionAPIKey, ListenerAPI]
1838
+ },
1839
+ extend(shell: Shell) {
1840
+ const api = shell.getAPI(SlotContributionAPIKey)
1841
+ api.contributeItem(shell, { value: 'item1' })
1842
+ api.contributeItem(shell, { value: 'item2' })
1843
+ }
1844
+ }
1845
+
1846
+ const ListenerAPI: SlotKey<{}> = { name: 'LISTENER_API' }
1847
+
1848
+ const ListenerEntryPoint: EntryPoint = {
1849
+ name: 'LISTENER_ENTRY_POINT',
1850
+ declareAPIs() {
1851
+ return [ListenerAPI]
1852
+ },
1853
+ getDependencyAPIs() {
1854
+ return [SlotContributionAPIKey]
1855
+ },
1856
+ // move to attach to make sure
1857
+ attach(shell: Shell) {
1858
+ const slotContributionAPI = shell.getAPI(SlotContributionAPIKey)
1859
+ effect(() => {
1860
+ slotContributionAPI.getItems()
1861
+ itemsSpy()
1862
+ })
1863
+ // contribute an api to make sure that the contribution depends on it and happens after.
1864
+ shell.contributeAPI(ListenerAPI, () => ({}))
1865
+ }
1866
+ }
1867
+
1868
+ const host = createAppHost([slotOwnerEntryPoint, ContributorEntryPoint, ListenerEntryPoint], {
1869
+ monitoring: {},
1870
+ plugins: {
1871
+ extensionSlot: {
1872
+ customCreateExtensionSlot: createDataStructure
1873
+ }
1874
+ }
1875
+ })
1876
+
1877
+ // first was created and then two time
1878
+ expect(itemsSpy).toBeCalledTimes(3)
1879
+
1880
+ await host.removeShells([ContributorEntryPoint.name])
1881
+ expect(itemsSpy).toBeCalledTimes(4)
1882
+ })
1883
+ })
1795
1884
  })
@@ -0,0 +1,65 @@
1
+ import { ExtensionItem } from '../src/API'
2
+
3
+ // Naive signals implementation
4
+ interface Signal<T> {
5
+ get(): T
6
+ set(value: T): void
7
+ }
8
+
9
+ let currentEffect: (() => void) | null = null
10
+
11
+ const signal = <T>(initialValue: T): Signal<T> => {
12
+ let value = initialValue
13
+ const subscribers = new Set<() => void>()
14
+
15
+ return {
16
+ get: () => {
17
+ if (currentEffect) {
18
+ subscribers.add(currentEffect)
19
+ }
20
+ return value
21
+ },
22
+ set: (newValue: T) => {
23
+ value = newValue
24
+ subscribers.forEach(fn => fn())
25
+ }
26
+ }
27
+ }
28
+
29
+ const effect = (fn: () => void): (() => void) => {
30
+ const execute = () => {
31
+ currentEffect = execute
32
+ fn()
33
+ currentEffect = null
34
+ }
35
+ execute()
36
+ return () => {
37
+ currentEffect = null
38
+ }
39
+ }
40
+
41
+ export const createSignalItemsDataStructure = () => {
42
+ let signalToTrack: Signal<ExtensionItem<any>[]> | null = null
43
+ return {
44
+ createDataStructure: <U>() => {
45
+ const itemsSignal = signal<ExtensionItem<U>[]>([])
46
+ signalToTrack = itemsSignal
47
+ return {
48
+ get: () => itemsSignal.get(),
49
+ add: (item: ExtensionItem<U>) => {
50
+ itemsSignal.set([...itemsSignal.get(), item])
51
+ },
52
+ discardBy: (predicate: (item: ExtensionItem<U>) => boolean) => {
53
+ itemsSignal.set(itemsSignal.get().filter(predicate))
54
+ }
55
+ }
56
+ },
57
+ getSignalToTrack: () => {
58
+ if (!signalToTrack) {
59
+ throw new Error('Signal to track is not set')
60
+ }
61
+ return signalToTrack
62
+ },
63
+ effect
64
+ }
65
+ }
@@ -0,0 +1,176 @@
1
+ import { createAppHost, SlotKey } from '../src'
2
+ import { EntryPoint, ExtensionItem, PrivateAppHost, PrivateExtensionSlot, Shell } from '../src/API'
3
+ import { createExtensionSlot } from '../src/extensionSlot'
4
+ import { addMockShell } from '../testKit'
5
+ import { createSignalItemsDataStructure } from './createSignalItemsDataStructure'
6
+
7
+ describe('ExtensionSlot', () => {
8
+ describe('Subscribe', () => {
9
+ it('should be invoked on contribution', () => {
10
+ const slotKey: SlotKey<{}> = {
11
+ name: 'mock_key'
12
+ }
13
+ const host = createAppHost([])
14
+ const shell = addMockShell(host)
15
+ const spy = jest.fn()
16
+ const slot = shell.declareSlot(slotKey) as PrivateExtensionSlot<{}>
17
+
18
+ slot.subscribe(spy)
19
+
20
+ slot.contribute(shell, () => ({}))
21
+
22
+ expect(spy).toBeCalledTimes(1)
23
+
24
+ slot.contribute(shell, () => ({}))
25
+
26
+ expect(spy).toBeCalledTimes(2)
27
+ })
28
+ it('should be invoked once when contribution is during installation phase', () => {
29
+ interface SlotItem {
30
+ name: string
31
+ }
32
+ const slotKey: SlotKey<SlotItem> = {
33
+ name: 'MOCK_SLOT'
34
+ }
35
+ interface MockContributionAPI {
36
+ contributeItem(fromShell: Shell, item: SlotItem): void
37
+ }
38
+
39
+ const ContributionAPI: SlotKey<MockContributionAPI> = {
40
+ name: 'CONTRIBUTION_API'
41
+ }
42
+
43
+ const spy = jest.fn()
44
+
45
+ const contributionEntryPoint: EntryPoint = {
46
+ name: 'PRIVATE CONTRIBUTION ENTRY POINT',
47
+ declareAPIs() {
48
+ return [ContributionAPI]
49
+ },
50
+ attach(shell) {
51
+ const slot = shell.declareSlot(slotKey) as PrivateExtensionSlot<SlotItem>
52
+ slot.subscribe(spy)
53
+ shell.contributeAPI(ContributionAPI, () => ({
54
+ contributeItem(fromShell, item) {
55
+ shell.getSlot(slotKey).contribute(fromShell, item)
56
+ }
57
+ }))
58
+ }
59
+ }
60
+
61
+ const API_A: SlotKey<{}> = { name: 'API_A' }
62
+
63
+ const itemContributionEntryPointA: EntryPoint = {
64
+ name: 'ITEM CONTRIBUTION ENTRY POINT A',
65
+ getDependencyAPIs() {
66
+ return [ContributionAPI]
67
+ },
68
+ declareAPIs(): SlotKey<{}>[] {
69
+ return [API_A]
70
+ },
71
+ attach(shell: Shell) {
72
+ shell.contributeAPI(API_A, () => ({}))
73
+ },
74
+ extend(shell: Shell) {
75
+ shell.getAPI(ContributionAPI).contributeItem(shell, { name: 'A.1' })
76
+ shell.getAPI(ContributionAPI).contributeItem(shell, { name: 'A.2' })
77
+ }
78
+ }
79
+
80
+ const itemContributionEntryPointB: EntryPoint = {
81
+ name: 'ITEM CONTRIBUTION ENTRY POINT B',
82
+ getDependencyAPIs() {
83
+ return [ContributionAPI, API_A]
84
+ },
85
+ extend(shell: Shell) {
86
+ shell.getAPI(ContributionAPI).contributeItem(shell, { name: 'B.1' })
87
+ shell.getAPI(ContributionAPI).contributeItem(shell, { name: 'B.2' })
88
+ }
89
+ }
90
+
91
+ createAppHost([contributionEntryPoint, itemContributionEntryPointA, itemContributionEntryPointB])
92
+
93
+ expect(spy).toBeCalledTimes(1)
94
+ })
95
+ })
96
+
97
+ describe('Custom Items Data Structure with Signals', () => {
98
+ it('should notify effect when items are added', () => {
99
+ const slotKey: SlotKey<{ value: string }> = { name: 'signal_slot' }
100
+ const host = createAppHost([]) as PrivateAppHost
101
+ const shell = addMockShell(host)
102
+
103
+ const { createDataStructure: dataStructure, getSignalToTrack, effect } = createSignalItemsDataStructure()
104
+ const effectSpy = jest.fn()
105
+
106
+ const slot = createExtensionSlot(slotKey, host, {
107
+ customCreateExtensionSlot: dataStructure
108
+ })
109
+
110
+ effect(() => {
111
+ getSignalToTrack().get()
112
+ effectSpy()
113
+ })
114
+
115
+ expect(effectSpy).toHaveBeenCalledTimes(1)
116
+
117
+ slot.contribute(shell, { value: 'item1' })
118
+ expect(effectSpy).toHaveBeenCalledTimes(2)
119
+
120
+ slot.contribute(shell, { value: 'item2' })
121
+ expect(effectSpy).toHaveBeenCalledTimes(3)
122
+ })
123
+
124
+ it('should notify effect when items are filtered/removed', () => {
125
+ const slotKey: SlotKey<{ id: number }> = { name: 'filter_signal_slot' }
126
+ const host = createAppHost([]) as PrivateAppHost
127
+ const shell = addMockShell(host)
128
+
129
+ const { createDataStructure: dataStructure, getSignalToTrack, effect } = createSignalItemsDataStructure()
130
+ const effectSpy = jest.fn()
131
+
132
+ const slot = createExtensionSlot(slotKey, host, {
133
+ customCreateExtensionSlot: dataStructure
134
+ })
135
+
136
+ effect(() => {
137
+ getSignalToTrack().get()
138
+ effectSpy()
139
+ })
140
+
141
+ expect(effectSpy).toHaveBeenCalledTimes(1)
142
+
143
+ slot.contribute(shell, { id: 1 })
144
+ slot.contribute(shell, { id: 2 })
145
+ expect(effectSpy).toHaveBeenCalledTimes(3)
146
+
147
+ slot.discardBy((item: ExtensionItem<{ id: number }>) => item.contribution.id === 1)
148
+ expect(effectSpy).toHaveBeenCalledTimes(4)
149
+ })
150
+
151
+ it('should track signal value correctly', () => {
152
+ const slotKey: SlotKey<{ name: string }> = { name: 'track_slot' }
153
+ const host = createAppHost([]) as PrivateAppHost
154
+ const shell = addMockShell(host)
155
+
156
+ const { createDataStructure: dataStructure, getSignalToTrack, effect } = createSignalItemsDataStructure()
157
+ let capturedItems: ExtensionItem<{ name: string }>[] = []
158
+
159
+ const slot = createExtensionSlot(slotKey, host, {
160
+ customCreateExtensionSlot: dataStructure
161
+ })
162
+
163
+ effect(() => {
164
+ capturedItems = getSignalToTrack().get()
165
+ })
166
+
167
+ slot.contribute(shell, { name: 'first' })
168
+ expect(capturedItems).toHaveLength(1)
169
+ expect(capturedItems[0].contribution.name).toBe('first')
170
+
171
+ slot.contribute(shell, { name: 'second' })
172
+ expect(capturedItems).toHaveLength(2)
173
+ expect(capturedItems[1].contribution.name).toBe('second')
174
+ })
175
+ })
176
+ })
package/testKit/index.ts CHANGED
@@ -4,13 +4,14 @@ import { AnySlotKey, AppHost, createAppHost as _createAppHost, EntryPointOrPacka
4
4
  import { createShellLogger } from '../src/loggers'
5
5
  import { emptyLoggerOptions } from './emptyLoggerOptions'
6
6
 
7
- export {createShellLogger}
7
+ export { createShellLogger }
8
8
  export { emptyLoggerOptions }
9
9
  export { AppHost } from '../src/index'
10
10
 
11
11
  export { withConsoleErrors } from './withConsoleErrors'
12
12
  export { withThrowOnError } from './withThrowOnError'
13
13
  export * from './mockPackage'
14
+ export { createSignalItemsDataStructure } from '../test/createSignalItemsDataStructure'
14
15
 
15
16
  export const createAppHost: typeof _createAppHost = (packages, options = emptyLoggerOptions) => {
16
17
  return _createAppHost(packages, options)
@@ -74,7 +75,10 @@ export function createAppHostWithPacts(packages: EntryPointOrPackage[], pacts: P
74
75
  }
75
76
  }
76
77
 
77
- return createAppHost([...packages, pactsEntryPoint], { ...emptyLoggerOptions, disableLayersValidation: true })
78
+ return createAppHost([...packages, pactsEntryPoint], {
79
+ ...emptyLoggerOptions,
80
+ disableLayersValidation: true
81
+ })
78
82
  }
79
83
 
80
84
  export async function createAppHostAndWaitForLoading(packages: EntryPointOrPackage[], pacts: PactAPIBase[]): Promise<AppHost> {
@@ -88,7 +92,7 @@ export async function createAppHostAndWaitForLoading(packages: EntryPointOrPacka
88
92
  setTimeout(() => {
89
93
  const readyAPIs = Array.from(globalThis.repluggableAppDebug.readyAPIs)
90
94
  const unreadyAPIs = declaredAPIs.filter(api => !readyAPIs.some(readyAPI => readyAPI.name === api.name))
91
- const rootUnreadyAPI = globalThis.repluggableAppDebug.utils.getRootUnreadyAPI();
95
+ const rootUnreadyAPI = globalThis.repluggableAppDebug.utils.getRootUnreadyAPI()
92
96
 
93
97
  reject(
94
98
  new Error(
@@ -118,7 +122,6 @@ in addition here's the full list of declared APIs that have not been contributed
118
122
  return Promise.race([timeoutPromise, loadingPromise]).then(() => appHost)
119
123
  }
120
124
 
121
-
122
125
  type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
123
126
  interface EntryPointOverrides extends Omit<EntryPoint, 'name'> {
124
127
  name?: EntryPoint['name']
@@ -159,8 +162,6 @@ export const addMockShell = (host: AppHost, entryPointOverrides: EntryPointOverr
159
162
  return shell
160
163
  }
161
164
 
162
-
163
-
164
165
  export function mockObservable<T>(value: T): ObservableState<T> {
165
166
  return {
166
167
  subscribe: () => {
@@ -171,4 +172,3 @@ export function mockObservable<T>(value: T): ObservableState<T> {
171
172
  }
172
173
  }
173
174
  }
174
-