Repository: effector/effector (ts)

Issue:
withRegion + clearNode incorrectly clears not related sample targets
<!--
Found a bug? Please fill out the sections below.
Be kind and objective when writing in text. Thanks!
-->

**What is the current behavior:**

Removing region using clearNode removes not only relations with region units but with any other units that are used in the same sample target array as them

**Please provide the steps to reproduce and if possible a minimal demo of the problem via https://share.effector.dev, https://codesandbox.io or similar**

https://share.effector.dev/CfOXurO3

1. Create unit A in region R
2. Create unit B outside of a region R
3. Create sample outside of a region R with any clock C and target [A, B]
4. Clear region R
5. Trigger clock C
6. Both units A and B are not triggered 

**What is the expected behavior:**

Expected unit B to be triggered since it and sample should not be cleared with region R

**Which versions of effector packages, and which browser and OS are affected by this issue? Did this work in previous versions of effector?:**

23.2.3

Pull request description:
Fix clearNode withRegion issue with external samples
Closes #1200

Patch:
diff --git a/src/effector/clearNode.ts b/src/effector/clearNode.ts
index b7fb10ab..7331b291 100644
--- a/src/effector/clearNode.ts
+++ b/src/effector/clearNode.ts
@@ -1,7 +1,7 @@
 import {Node, NodeUnit} from './index.h'
-import {getGraph, getOwners, getLinks, getSubscribers} from './getter'
+import {getGraph, getOwners, getLinks} from './getter'
 import {is} from './is'
-import {removeItem} from './collection'
+import {includes, removeItem} from './collection'
 import {CROSSLINK} from './tag'
 
 const removeFromNode = (currentNode: Node, targetNode: Node) => {
@@ -9,10 +9,22 @@ const removeFromNode = (currentNode: Node, targetNode: Node) => {
   removeItem(getOwners(currentNode), targetNode)
   removeItem(getLinks(currentNode), targetNode)
 }
+/** These nodes should be cleared but dissalow clearing of any links */
+const nonPassableNodes = [
+  'on',
+  'reset',
+  'sample',
+  'split',
+  'merge',
+  'guard',
+  'forward',
+]
 const clearNodeNormalized = (
   targetNode: Node,
   deep: boolean,
   isDomainUnit: boolean,
+  regionNode: Node | null,
+  extractOnly: boolean,
 ) => {
   targetNode.next.length = 0
   targetNode.seq.length = 0
@@ -20,18 +32,38 @@ const clearNodeNormalized = (
   targetNode.scope = null
   let currentNode
   let list = getLinks(targetNode)
-  while ((currentNode = list.pop())) {
-    removeFromNode(currentNode, targetNode)
-    if (
-      deep ||
-      (isDomainUnit && targetNode.meta.op !== 'sample') ||
-      currentNode.family.type === CROSSLINK
-    ) {
-      clearNodeNormalized(
-        currentNode,
-        deep,
-        currentNode.meta.op !== 'on' && isDomainUnit,
-      )
+  const isRegionNode = targetNode.meta.isRegion
+  const nextRegionNode = isRegionNode ? targetNode : regionNode
+  if (list.length > 0) {
+    const targetIsOp = includes(nonPassableNodes, targetNode.meta.op)
+    const canGoDeep = !isRegionNode && !extractOnly
+    const domainSampleEdgeCase = canGoDeep && isDomainUnit && !targetIsOp
+    while ((currentNode = list.pop())) {
+      const isTrigger = includes(currentNode.next, targetNode)
+      removeFromNode(currentNode, targetNode)
+      if (isRegionNode) {
+        clearNodeNormalized(currentNode, false, false, targetNode, true)
+      }
+      if (!isTrigger) {
+        currentNode.family.triggers -= 1
+      }
+      if (
+        deep ||
+        domainSampleEdgeCase ||
+        (canGoDeep && currentNode.family.type === CROSSLINK && !targetIsOp) ||
+        (extractOnly &&
+          includes(nonPassableNodes, currentNode.meta.op) &&
+          ((isTrigger && currentNode.next.length === 0) ||
+            (!isTrigger && currentNode.family.triggers <= 0)))
+      ) {
+        clearNodeNormalized(
+          currentNode,
+          deep,
+          isDomainUnit && currentNode.meta.op !== 'on',
+          nextRegionNode,
+          extractOnly,
+        )
+      }
     }
   }
   list = getOwners(targetNode)
@@ -41,7 +73,9 @@ const clearNodeNormalized = (
       clearNodeNormalized(
         currentNode,
         deep,
-        currentNode.meta.op !== 'on' && isDomainUnit,
+        currentNode.meta.op !== 'on',
+        nextRegionNode,
+        extractOnly,
       )
     }
   }
@@ -58,9 +92,7 @@ export const clearNode = (
   let isDomainUnit = false
   //@ts-expect-error
   if (graphite.ownerSet) graphite.ownerSet.delete(graphite)
-  if (is.store(graphite)) {
-    clearMap(getSubscribers(graphite))
-  } else if (is.domain(graphite)) {
+  if (is.domain(graphite)) {
     isDomainUnit = true
     const history = graphite.history
     clearMap(history.events)
@@ -68,5 +100,5 @@ export const clearNode = (
     clearMap(history.stores)
     clearMap(history.domains)
   }
-  clearNodeNormalized(getGraph(graphite), !!deep, isDomainUnit)
+  clearNodeNormalized(getGraph(graphite), !!deep, isDomainUnit, null, false)
 }
diff --git a/src/effector/createEffect.ts b/src/effector/createEffect.ts
index 2bac86a2..0cd05c03 100644
--- a/src/effector/createEffect.ts
+++ b/src/effector/createEffect.ts
@@ -145,6 +145,7 @@ export function createEffect<Params, Done, Fail = Error>(
       ),
     ],
     meta: {op: 'fx', fx: 'runner'},
+    regional: true,
   })
   node.scope.runner = runner
   add(
diff --git a/src/effector/createNode.ts b/src/effector/createNode.ts
index 3c053c2b..e241a770 100644
--- a/src/effector/createNode.ts
+++ b/src/effector/createNode.ts
@@ -51,6 +51,7 @@ export function createNode({
     meta,
     scope,
     family: {
+      triggers: sources.length,
       type: familyRaw.type || CROSSLINK,
       links,
       owners,
diff --git a/src/effector/createUnit.ts b/src/effector/createUnit.ts
index 6f7ef769..5904c5ce 100644
--- a/src/effector/createUnit.ts
+++ b/src/effector/createUnit.ts
@@ -30,21 +30,14 @@ import {
 import {createName, generateErrorTitle} from './naming'
 import {createLinkNode} from './forward'
 import {watchUnit} from './watch'
-import {createSubscription} from './subscription'
 import {readTemplate, readSidRoot, reportDeclaration} from './region'
-import {
-  getSubscribers,
-  getStoreState,
-  getGraph,
-  getParent,
-  setMeta,
-  getMeta,
-} from './getter'
+import {getStoreState, getGraph, getParent, setMeta, getMeta} from './getter'
 import {assert, deprecate, printErrorWithStack} from './throw'
 import {DOMAIN, STORE, EVENT, MAP, STACK, REG_A} from './tag'
 import {applyTemplate} from './template'
 import {forEach} from './collection'
 import {flattenConfig} from './config'
+import {clearNode} from './clearNode'
 
 export const applyParentHook = (
   source: CommonUnit,
@@ -223,10 +216,7 @@ function on<State>(
   )
   forEach(Array.isArray(nodeSet) ? nodeSet : [nodeSet], trigger => {
     store.off(trigger)
-    getSubscribers(store).set(
-      trigger,
-      createSubscription(updateStore(trigger, store, 'on', callARegStack, fn)),
-    )
+    updateStore(trigger, store, 'on', callARegStack, fn)
   })
   return store
 }
@@ -258,7 +248,6 @@ export function createStore<State>(
   deprecate(!skipVoidTrueSet, '{skipVoid: true}', 'updateFilter', errorTitle)
 
   const store = {
-    subscribers: new Map(),
     updates,
     defaultState,
     stateRef: plainState,
@@ -308,10 +297,12 @@ export function createStore<State>(
       return on(store, '.on', nodeSet, fn, errorTitle)
     },
     off(unit: CommonUnit) {
-      const currentSubscription = getSubscribers(store).get(unit)
-      if (currentSubscription) {
-        currentSubscription()
-        getSubscribers(store).delete(unit)
+      const triggerUnitId = getGraph(unit).id
+      const oldLink = getGraph(store).family.links.find(
+        e => e.meta.onTrigger === triggerUnitId,
+      )
+      if (oldLink) {
+        clearNode(oldLink)
       }
       return store
     },
@@ -468,5 +459,9 @@ const updateStore = (
     node,
     is.store(from) && getStoreState(from),
   )
-  return createLinkNode(from, store, node, op, fn)
+  const result = createLinkNode(from, store, node, op, fn)
+  if (op !== MAP) {
+    setMeta(result, 'onTrigger', getGraph(from).id)
+  }
+  return result
 }
diff --git a/src/effector/getter.ts b/src/effector/getter.ts
index bb288ecd..97944ac8 100644
--- a/src/effector/getter.ts
+++ b/src/effector/getter.ts
@@ -6,8 +6,7 @@ export const getGraph = (graph: NodeUnit): Node =>
 export const getOwners = (node: Node) => node.family.owners
 export const getLinks = (node: Node) => node.family.links
 export const getStoreState = (store: Store<any>): StateRef => store.stateRef
-export const getValue = (stack: any) => stack.value
-export const getSubscribers = (store: Store<any>) => store.subscribers
+export const getValue = <T>(stack: {value: T}) => stack.value
 export const getParent = (unit: any) => unit.parent
 export const getForkPage = (val: any): Scope | void => val.scope
 export const getMeta = (unit: NodeUnit, field: string) =>
diff --git a/src/effector/index.h.ts b/src/effector/index.h.ts
index 7cc39419..73200d18 100644
--- a/src/effector/index.h.ts
+++ b/src/effector/index.h.ts
@@ -52,6 +52,8 @@ export type Node = {
   // reg: {[id: string]: StateRef}
   meta: {[tag: string]: any}
   family: {
+    /** amount of triggers during init */
+    triggers: number
     type: 'regular' | 'crosslink' | 'domain'
     links: Node[]
     owners: Node[]
diff --git a/src/effector/region.ts b/src/effector/region.ts
index 4a8e0ead..d15013f7 100644
--- a/src/effector/region.ts
+++ b/src/effector/region.ts
@@ -2,6 +2,7 @@ import type {Template} from '../forest/index.h'
 import type {NodeUnit, Node, ID} from './index.h'
 import {getParent, getGraph} from './getter'
 import {createNode} from './createNode'
+import {is} from './is'
 
 type DeclarationSourceReporter = (
   node: Node | 'region',
@@ -48,10 +49,15 @@ export const readSidRoot = (sid?: string | null) => {
 }
 
 export function withRegion<T = void>(unit: NodeUnit, cb: () => T): T {
-  const meta = getGraph(unit).meta || {}
+  const node = getGraph(unit)
+  const meta = node.meta || {}
+
+  if (!is.domain(unit)) {
+    meta.isRegion = true
+  }
 
   regionStack = {
-    id: getGraph(unit).id,
+    id: node.id,
     parent: regionStack,
     value: unit,
     template: meta.template || readTemplate(),
@@ -81,6 +87,7 @@ export const withFactory = ({
 }) => {
   const factoryRootNode = createNode({
     meta: {sidRoot: readSidRoot(sid), sid, name, loc, method, type: 'factory'},
+    regional: true,
   })
 
   return withRegion(factoryRootNode, fn)
diff --git a/src/effector/sample.ts b/src/effector/sample.ts
index 1805f3c0..a5191a60 100644
--- a/src/effector/sample.ts
+++ b/src/effector/sample.ts
@@ -1,5 +1,5 @@
 import type {Cmd, StateRef} from './index.h'
-import type {CommonUnit, DataCarrier} from './unit.h'
+import type {CommonUnit, DataCarrier, Store} from './unit.h'
 import {combine} from './combine'
 import {mov, userFnCall, read, calc} from './step'
 import {createStateRef, readRef} from './stateRef'
@@ -20,7 +20,6 @@ import {createNode} from './createNode'
 import {assert, deprecate} from './throw'
 import {forEach} from './collection'
 import {STACK, VALUE} from './tag'
-import {merge} from './merge'
 import {applyTemplate} from './template'
 import {own} from './own'
 import {createLinkNode} from './forward'
@@ -119,7 +118,7 @@ export const createSampling = (
   } else {
     assertNodeSet(clock, errorTitle, 'clock')
     if (Array.isArray(clock)) {
-      clock = merge(clock as CommonUnit[])
+      clock = createLinkNode(clock as CommonUnit[], [], [], method)
     }
   }
   if (sourceIsClock) {
@@ -238,7 +237,10 @@ const syncSourceState = (
         mov({from: STACK, target: sourceRef}),
         mov({from: VALUE, store: true, target: hasSource}),
       ],
-      family: {owners: [source, target, clock], links: target},
+      family: {
+        owners: [...new Set([source, target, clock].flat())],
+        links: target,
+      },
       meta: {op: method},
       regional: true,
     })
diff --git a/src/effector/unit.h.ts b/src/effector/unit.h.ts
index e76e78ab..d5a0484a 100644
--- a/src/effector/unit.h.ts
+++ b/src/effector/unit.h.ts
@@ -29,7 +29,6 @@ export interface Event<E> extends Unit {
 }
 
 export interface Store<State> extends Unit {
-  subscribers: Map<CommonUnit, Subscription>
   id: string
   stateRef: StateRef
   reset(...units: CommonUnit[]): Store<State>


Test patch:
diff --git a/src/effector/__tests__/clearNode.test.ts b/src/effector/__tests__/clearNode.test.ts
index 10253c37..8255d801 100644
--- a/src/effector/__tests__/clearNode.test.ts
+++ b/src/effector/__tests__/clearNode.test.ts
@@ -12,6 +12,8 @@ import {
   createNode,
   Node,
   step,
+  EventCallable,
+  merge,
 } from 'effector'
 import {argumentHistory, muteErrors} from 'effector/fixtures'
 
@@ -969,3 +971,712 @@ describe('supports sample in withRegion', () => {
     expect(argumentHistory(fnTarget)).toEqual([0, 10])
   })
 })
+
+describe.each([{regionWrap: false}, {regionWrap: true}])(
+  'external connections survival, wrap in region: $regionWrap',
+  ({regionWrap}) => {
+    /**
+     * Ability to remove link from regional unit
+     * to external declared outside of region
+     * (e.g. `$external.on(regional, ...)`)
+     *
+     * When false, it's better to always declare
+     * links with regional units in clock
+     * in the region itself
+     **/
+    const KILL_EXTERNAL = true
+    const EXTERNAL_UNIT_CAN_KILL = true
+    /** When sample filter becomes dead it kills a whole sample */
+    const SAMPLE_FILTER_SUPPORTED = false
+
+    let sampleFn: jest.Mock<number, [number]>
+    let externalTriggerFn: jest.Mock<any, any>
+    let externalTargetFn: jest.Mock<any, any>
+    let regionalUnitFn: jest.Mock<any, any>
+    let region: Node
+    let externalTrigger: EventCallable<number>
+    let externalTarget: EventCallable<number>
+    let parentRegion: Node
+    beforeEach(() => {
+      sampleFn = jest.fn(x => x)
+      externalTriggerFn = jest.fn()
+      externalTargetFn = jest.fn()
+      regionalUnitFn = jest.fn()
+      parentRegion = createNode()
+      ;[region, externalTrigger, externalTarget] = optionalRegionWrap(() => [
+        createNode(),
+        createEvent<number>(),
+        createEvent<number>(),
+      ])
+    })
+    function optionalRegionWrap<T>(cb: () => T) {
+      if (!regionWrap) return cb()
+      return withRegion(parentRegion, () => cb())
+    }
+    describe('regional unit in external sample target', () => {
+      test('single unit in target (should destroy the link)', () => {
+        optionalRegionWrap(() => {
+          const regionalTarget = withRegion(region, () => createEvent<number>())
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          sample({
+            clock: externalTrigger,
+            fn: upd => sampleFn(upd),
+            target: regionalTarget,
+          })
+        })
+        externalTrigger(0)
+        clearNode(region)
+        externalTrigger(1)
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          sampleFn: argumentHistory(sampleFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1],
+          sampleFn: KILL_EXTERNAL ? [0] : [0, 1],
+        })
+      })
+      test('single unit in target array (should destroy the link)', () => {
+        optionalRegionWrap(() => {
+          const regionalTarget = withRegion(region, () => createEvent<number>())
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          sample({
+            clock: externalTrigger,
+            fn: upd => sampleFn(upd),
+            target: [regionalTarget],
+          })
+        })
+        externalTrigger(0)
+        clearNode(region)
+        externalTrigger(1)
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          sampleFn: argumentHistory(sampleFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1],
+          sampleFn: KILL_EXTERNAL ? [0] : [0, 1],
+        })
+      })
+      test('not a single unit in target array (should keep the link)', () => {
+        optionalRegionWrap(() => {
+          const regionalTarget = withRegion(region, () => createEvent<number>())
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          regionalTarget.watch(upd => regionalUnitFn(upd))
+          sample({
+            clock: externalTrigger,
+            fn: upd => sampleFn(upd),
+            target: [externalTarget, regionalTarget],
+          })
+        })
+        externalTrigger(0)
+        clearNode(region)
+        externalTrigger(1)
+        externalTarget(2)
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+          sampleFn: argumentHistory(sampleFn),
+          regionalUnitFn: argumentHistory(regionalUnitFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1],
+          externalTargetFn: [0, 1, 2],
+          sampleFn: [0, 1],
+          regionalUnitFn: [0],
+        })
+      })
+      test('last unit in target array is regional (should destroy the link)', () => {
+        optionalRegionWrap(() => {
+          const regionalTarget = withRegion(region, () => createEvent<number>())
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          regionalTarget.watch(upd => regionalUnitFn(upd))
+          sample({
+            clock: externalTrigger,
+            fn: upd => sampleFn(upd),
+            target: [externalTarget, regionalTarget],
+          })
+        })
+        externalTrigger(0)
+        clearNode(externalTarget)
+        externalTrigger(1)
+        clearNode(region)
+        externalTrigger(2)
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+          sampleFn: argumentHistory(sampleFn),
+          regionalUnitFn: argumentHistory(regionalUnitFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1, 2],
+          externalTargetFn: [0],
+          sampleFn: EXTERNAL_UNIT_CAN_KILL ? [0] : [0, 1],
+          regionalUnitFn: EXTERNAL_UNIT_CAN_KILL ? [0] : [0, 1],
+        })
+      })
+      test('last two units in target array are regional (should keep the link)', () => {
+        const regionB = optionalRegionWrap(() => {
+          const regionalTarget = withRegion(region, () => createEvent<number>())
+          const regionB = createNode()
+          const regionalTargetB = withRegion(regionB, () =>
+            createEvent<number>(),
+          )
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          regionalTarget.watch(upd => regionalUnitFn(upd))
+          sample({
+            clock: externalTrigger,
+            fn: upd => sampleFn(upd),
+            target: [externalTarget, regionalTargetB, regionalTarget],
+          })
+          return regionB
+        })
+        externalTrigger(0)
+        // clearNode(regionalTargetB)
+        clearNode(regionB)
+        externalTrigger(1)
+        clearNode(region)
+        externalTrigger(2)
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+          sampleFn: argumentHistory(sampleFn),
+          regionalUnitFn: argumentHistory(regionalUnitFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1, 2],
+          externalTargetFn: [0, 1, 2],
+          sampleFn: [0, 1, 2],
+          regionalUnitFn: [0, 1],
+        })
+      })
+      test('last unit in target array is not regional (should destroy the link)', () => {
+        // This test may not work because of history of default behavior of clearNode
+        const regionalTarget = optionalRegionWrap(() => {
+          const regionalTarget = withRegion(region, () => createEvent<number>())
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          regionalTarget.watch(upd => regionalUnitFn(upd))
+          sample({
+            clock: externalTrigger,
+            fn: upd => sampleFn(upd),
+            target: [externalTarget, regionalTarget],
+          })
+          return regionalTarget
+        })
+        externalTrigger(0)
+        clearNode(region)
+        externalTrigger(1)
+        clearNode(externalTarget)
+        externalTrigger(2)
+        regionalTarget(3)
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+          sampleFn: argumentHistory(sampleFn),
+          regionalUnitFn: argumentHistory(regionalUnitFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1, 2],
+          externalTargetFn: [0, 1],
+          sampleFn: [0, 1],
+          regionalUnitFn: EXTERNAL_UNIT_CAN_KILL ? [0] : [0, 3],
+        })
+      })
+    })
+    describe('regional unit in external sample clock', () => {
+      test('single unit in clock (should destroy the link)', () => {
+        const regionalTrigger = optionalRegionWrap(() => {
+          const regionalTrigger = withRegion(region, () =>
+            createEvent<number>(),
+          )
+          regionalTrigger.watch(upd => regionalUnitFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          sample({
+            clock: regionalTrigger,
+            fn: upd => sampleFn(upd),
+            target: externalTarget,
+          })
+          return regionalTrigger
+        })
+        regionalTrigger(0)
+        clearNode(region)
+        regionalTrigger(1)
+        externalTarget(2)
+        expect({
+          regionalTriggerFn: argumentHistory(regionalUnitFn),
+          sampleFn: argumentHistory(sampleFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+        }).toEqual({
+          regionalTriggerFn: [0],
+          sampleFn: [0],
+          externalTargetFn: [0, 2],
+        })
+      })
+      test('single unit in clock array (should destroy the link)', () => {
+        const regionalTrigger = optionalRegionWrap(() => {
+          const regionalTrigger = withRegion(region, () =>
+            createEvent<number>(),
+          )
+          regionalTrigger.watch(upd => regionalUnitFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          sample({
+            clock: [regionalTrigger],
+            fn: upd => sampleFn(upd),
+            target: externalTarget,
+          })
+          return regionalTrigger
+        })
+        regionalTrigger(0)
+        clearNode(region)
+        regionalTrigger(1)
+        externalTarget(2)
+        expect({
+          regionalTriggerFn: argumentHistory(regionalUnitFn),
+          sampleFn: argumentHistory(sampleFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+        }).toEqual({
+          regionalTriggerFn: [0],
+          sampleFn: [0],
+          externalTargetFn: [0, 2],
+        })
+      })
+      test('not a single unit in clock array (should keep the link)', () => {
+        const regionalTrigger = optionalRegionWrap(() => {
+          const regionalTrigger = withRegion(region, () =>
+            createEvent<number>(),
+          )
+          regionalTrigger.watch(upd => regionalUnitFn(upd))
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          sample({
+            clock: [externalTrigger, regionalTrigger],
+            fn: upd => sampleFn(upd),
+            target: externalTarget,
+          })
+          return regionalTrigger
+        })
+        regionalTrigger(0)
+        externalTrigger(1)
+        clearNode(region)
+        regionalTrigger(2)
+        externalTrigger(3)
+        externalTarget(4)
+        expect({
+          regionalTriggerFn: argumentHistory(regionalUnitFn),
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          sampleFn: argumentHistory(sampleFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+        }).toEqual({
+          regionalTriggerFn: [0],
+          externalTriggerFn: [1, 3],
+          sampleFn: [0, 1, 3],
+          externalTargetFn: [0, 1, 3, 4],
+        })
+      })
+      test('last unit in clock array is regional (should destroy the link)', () => {
+        const regionalTrigger = optionalRegionWrap(() => {
+          const regionalTrigger = withRegion(region, () =>
+            createEvent<number>(),
+          )
+          regionalTrigger.watch(upd => regionalUnitFn(upd))
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          sample({
+            clock: [externalTrigger, regionalTrigger],
+            fn: upd => sampleFn(upd),
+            target: externalTarget,
+          })
+          return regionalTrigger
+        })
+        regionalTrigger(0)
+        externalTrigger(1)
+        clearNode(externalTrigger)
+        regionalTrigger(2)
+        externalTrigger(3)
+        externalTarget(4)
+        clearNode(region)
+        regionalTrigger(5)
+        externalTrigger(6)
+        externalTarget(7)
+        expect({
+          regionalTriggerFn: argumentHistory(regionalUnitFn),
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          sampleFn: argumentHistory(sampleFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+        }).toEqual({
+          regionalTriggerFn: [0, 2],
+          externalTriggerFn: [1],
+          sampleFn: EXTERNAL_UNIT_CAN_KILL ? [0, 1] : [0, 1, 2],
+          externalTargetFn: EXTERNAL_UNIT_CAN_KILL
+            ? [0, 1, 4, 7]
+            : [0, 1, 2, 4, 7],
+        })
+      })
+      test('last unit in clock array is not regional (should destroy the link)', () => {
+        // This test may not work because of history of default behavior of clearNode
+        const regionalTrigger = optionalRegionWrap(() => {
+          const regionalTrigger = withRegion(region, () =>
+            createEvent<number>(),
+          )
+          regionalTrigger.watch(upd => regionalUnitFn(upd))
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          externalTarget.watch(upd => externalTargetFn(upd))
+          sample({
+            clock: [externalTrigger, regionalTrigger],
+            fn: upd => sampleFn(upd),
+            target: externalTarget,
+          })
+          return regionalTrigger
+        })
+        regionalTrigger(0)
+        externalTrigger(1)
+        clearNode(region)
+        regionalTrigger(2)
+        externalTrigger(3)
+        externalTarget(4)
+        clearNode(externalTrigger)
+        regionalTrigger(5)
+        externalTrigger(6)
+        externalTarget(7)
+        expect({
+          regionalTriggerFn: argumentHistory(regionalUnitFn),
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          sampleFn: argumentHistory(sampleFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+        }).toEqual({
+          regionalTriggerFn: [0],
+          externalTriggerFn: [1, 3],
+          sampleFn: [0, 1, 3],
+          externalTargetFn: [0, 1, 3, 4, 7],
+        })
+      })
+    })
+    test('regional unit in external sample filter (should destroy the link)', () => {
+      optionalRegionWrap(() => {
+        const $filter = withRegion(region, () => createStore(true))
+        externalTrigger.watch(upd => externalTriggerFn(upd))
+        externalTarget.watch(upd => externalTargetFn(upd))
+        sample({
+          clock: externalTrigger,
+          filter: $filter,
+          fn: upd => sampleFn(upd),
+          target: externalTarget,
+        })
+      })
+      externalTrigger(0)
+      clearNode(region)
+      externalTrigger(1)
+      externalTarget(2)
+      expect({
+        externalTriggerFn: argumentHistory(externalTriggerFn),
+        sampleFn: argumentHistory(sampleFn),
+        externalTargetFn: argumentHistory(externalTargetFn),
+      }).toEqual({
+        externalTriggerFn: [0, 1],
+        sampleFn: KILL_EXTERNAL && SAMPLE_FILTER_SUPPORTED ? [0] : [0, 1],
+        externalTargetFn:
+          KILL_EXTERNAL && SAMPLE_FILTER_SUPPORTED ? [0, 2] : [0, 1, 2],
+      })
+    })
+    describe('regional unit in external merge clock', () => {
+      test('single unit in clock array (should keep target unit)', () => {
+        const [regionalTrigger, mergeTarget] = optionalRegionWrap(() => {
+          const regionalTrigger = withRegion(region, () =>
+            createEvent<number>(),
+          )
+          const mergeTarget = merge([regionalTrigger])
+          regionalTrigger.watch(upd => regionalUnitFn(upd))
+          mergeTarget.watch(upd => externalTargetFn(upd))
+          return [regionalTrigger, mergeTarget] as const
+        })
+        regionalTrigger(0)
+        clearNode(region)
+        regionalTrigger(1)
+        expect({
+          regionalTriggerFn: argumentHistory(regionalUnitFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+        }).toEqual({
+          regionalTriggerFn: [0],
+          externalTargetFn: [0],
+        })
+
+        const mergeTargetNode = (mergeTarget as any).graphite as Node
+
+        expect({
+          mergeTargetOwners: mergeTargetNode.family.owners.length,
+          mergeTargetNext: mergeTargetNode.next.length,
+        }).toEqual({
+          mergeTargetOwners: regionWrap ? 1 : 0,
+          mergeTargetNext: 1,
+        })
+      })
+      test('not a single unit in clock array (should keep target unit)', () => {
+        const regionalTrigger = optionalRegionWrap(() => {
+          const regionalTrigger = withRegion(region, () =>
+            createEvent<number>(),
+          )
+          const mergeTarget = merge([regionalTrigger, externalTrigger])
+          regionalTrigger.watch(upd => regionalUnitFn(upd))
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          mergeTarget.watch(upd => externalTargetFn(upd))
+          return regionalTrigger
+        })
+        regionalTrigger(0)
+        externalTrigger(1)
+        clearNode(region)
+        regionalTrigger(2)
+        externalTrigger(3)
+        expect({
+          regionalTriggerFn: argumentHistory(regionalUnitFn),
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          externalTargetFn: argumentHistory(externalTargetFn),
+        }).toEqual({
+          regionalTriggerFn: [0],
+          externalTriggerFn: [1, 3],
+          externalTargetFn: [0, 1, 3],
+        })
+      })
+    })
+
+    test('withRegion + on (unsubscribe edge case)', () => {
+      const {$count, $str} = optionalRegionWrap(() => {
+        return withRegion(region, () => {
+          const inc = createEvent()
+          const $count = createStore(0)
+          $count.on(inc, n => n + 1)
+          const $str = $count.map(n => String(n))
+
+          inc()
+
+          $count.on(inc, n => n + 1)
+
+          inc()
+
+          return {$count, $str}
+        })
+      })
+
+      expect({
+        count: $count.getState(),
+        str: $str.getState(),
+      }).toEqual({count: 2, str: '2'})
+    })
+
+    test('regional .on support', () => {
+      const handlerFn = jest.fn((x: number) => x)
+      const replace = optionalRegionWrap(() => {
+        const $count = createStore(0)
+        const replace = createEvent<number>()
+        withRegion(region, () => {
+          $count.on(replace, (_, upd) => handlerFn(upd))
+        })
+        return replace
+      })
+
+      replace(1)
+      clearNode(region)
+      replace(2)
+
+      expect(argumentHistory(handlerFn)).toEqual([1])
+    })
+
+    test('external .on support', () => {
+      const handlerFn = jest.fn((x: number) => x)
+      const [$count, replace] = optionalRegionWrap(() => {
+        const $count = createStore(0)
+        const replace = withRegion(region, () => createEvent<number>())
+        $count.on(replace, (_, upd) => handlerFn(upd))
+        return [$count, replace] as const
+      })
+      replace(1)
+      clearNode(region)
+      replace(2)
+
+      const node: Node = ($count as any).graphite
+
+      expect(argumentHistory(handlerFn)).toEqual([1])
+      /** .updates and reinit */
+      expect(node.family.links.length).toBe(KILL_EXTERNAL ? 2 : 3)
+    })
+
+    test('.on memory leaks', () => {
+      const $count = optionalRegionWrap(() => {
+        const $count = createStore(0)
+        return $count
+      })
+      function factory() {
+        const region = createNode()
+
+        const inc = withRegion(region, () => {
+          const inc = createEvent()
+          $count.on(inc, x => x + 1)
+          return inc
+        })
+
+        inc()
+
+        return {inc, region}
+      }
+
+      for (let i = 0; i < 10; i++) {
+        const {region} = optionalRegionWrap(() => factory())
+        clearNode(region)
+      }
+
+      const node: Node = ($count as any).graphite
+
+      /** only .updates */
+      expect(node.next.length).toBe(1)
+      /** .updates and reinit */
+      expect(node.family.links.length).toBe(2)
+    })
+
+    /** So nested models will NOT be cleared correctly without region: true */
+    test('inner regions will survive with regional: false', () => {
+      const innerEvent = optionalRegionWrap(() => {
+        return withRegion(region, () => {
+          const innerRegion = createNode()
+          return withRegion(innerRegion, () => createEvent<number>())
+        })
+      })
+      innerEvent.watch(upd => regionalUnitFn(upd))
+
+      innerEvent(0)
+      clearNode(region)
+      innerEvent(1)
+
+      expect(argumentHistory(regionalUnitFn)).toEqual([0, 1])
+    })
+
+    test('inner regions will not survive with regional: true', () => {
+      const innerEvent = optionalRegionWrap(() => {
+        return withRegion(region, () => {
+          const innerRegion = createNode({regional: true})
+          return withRegion(innerRegion, () => createEvent<number>())
+        })
+      })
+      innerEvent.watch(upd => regionalUnitFn(upd))
+
+      innerEvent(0)
+      clearNode(region)
+      innerEvent(1)
+
+      expect(argumentHistory(regionalUnitFn)).toEqual([0])
+    })
+
+    describe('external unit should survive', () => {
+      let handlerFn: jest.Mock<number, [number]>
+      beforeEach(() => {
+        handlerFn = jest.fn(x => x)
+      })
+      test('store .map', () => {
+        const inc = optionalRegionWrap(() => {
+          const inc = createEvent()
+          const $external = createStore(0)
+          $external.on(inc, x => x + 1)
+          const $regional = withRegion(region, () => {
+            return $external.map(n => handlerFn(n))
+          })
+          $external.watch(upd => externalTriggerFn(upd))
+          $regional.watch(upd => regionalUnitFn(upd))
+          return inc
+        })
+
+        inc()
+
+        clearNode(region)
+
+        inc()
+
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          regionalUnitFn: argumentHistory(regionalUnitFn),
+          handlerFn: argumentHistory(handlerFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1, 2],
+          regionalUnitFn: [0, 1],
+          handlerFn: [0, 1],
+        })
+      })
+      test('combine', () => {
+        const inc = optionalRegionWrap(() => {
+          const inc = createEvent()
+          const $external = createStore(0)
+          $external.on(inc, x => x + 1)
+          const $regional = withRegion(region, () => {
+            return combine($external, n => handlerFn(n))
+          })
+          $external.watch(upd => externalTriggerFn(upd))
+          $regional.watch(upd => regionalUnitFn(upd))
+          return inc
+        })
+
+        inc()
+
+        clearNode(region)
+
+        inc()
+
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          regionalUnitFn: argumentHistory(regionalUnitFn),
+          handlerFn: argumentHistory(handlerFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1, 2],
+          regionalUnitFn: [0, 1],
+          handlerFn: [0, 1],
+        })
+      })
+      test('event .map', () => {
+        optionalRegionWrap(() => {
+          const regionalTarget = withRegion(region, () => {
+            return externalTrigger.map(n => handlerFn(n))
+          })
+          externalTrigger.watch(upd => externalTriggerFn(upd))
+          regionalTarget.watch(upd => regionalUnitFn(upd))
+        })
+
+        externalTrigger(0)
+
+        clearNode(region)
+
+        externalTrigger(1)
+
+        expect({
+          externalTriggerFn: argumentHistory(externalTriggerFn),
+          regionalUnitFn: argumentHistory(regionalUnitFn),
+          handlerFn: argumentHistory(handlerFn),
+        }).toEqual({
+          externalTriggerFn: [0, 1],
+          regionalUnitFn: [0],
+          handlerFn: [0],
+        })
+      })
+      test('event .prepend', () => {
+        const regionalTrigger = optionalRegionWrap(() => {
+          const regionalTrigger = withRegion(region, () => {
+            return externalTarget.prepend((n: number) => handlerFn(n))
+          })
+          externalTarget.watch(upd => externalTargetFn(upd))
+          regionalTrigger.watch(upd => regionalUnitFn(upd))
+          return regionalTrigger
+        })
+
+        regionalTrigger(0)
+
+        clearNode(region)
+
+        regionalTrigger(1)
+        externalTarget(2)
+
+        expect({
+          externalTargetFn: argumentHistory(externalTargetFn),
+          regionalUnitFn: argumentHistory(regionalUnitFn),
+          handlerFn: argumentHistory(handlerFn),
+        }).toEqual({
+          externalTargetFn: [0, 2],
+          regionalUnitFn: [0],
+          handlerFn: [0],
+        })
+      })
+    })
+  },
+)
diff --git a/src/effector/__tests__/inspect.test.ts b/src/effector/__tests__/inspect.test.ts
index 25cb5a52..f0bbc0ea 100644
--- a/src/effector/__tests__/inspect.test.ts
+++ b/src/effector/__tests__/inspect.test.ts
@@ -450,6 +450,7 @@ describe('inspectGraph API', () => {
           source: $source,
           target: targetEvent,
         },
+        isRegion: true,
       })
       expect(argumentHistory(declared)).toMatchInlineSnapshot(`
         Array [
@@ -557,11 +558,12 @@ describe('inspectGraph API', () => {
         id: expect.any(String),
         meta: {
           region: 'inner',
+          isRegion: true,
         },
         region: {
           type: 'region',
           id: expect.any(String),
-          meta: {region: 'outer'},
+          meta: {region: 'outer', isRegion: true},
           region: {
             type: 'factory',
             id: expect.any(String),